ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从回测到实盘:量化交易三步法实战指南

从回测到实盘:量化交易三步法实战指南 程序员进场做量化绝大多数人不是从开户开始的是从回测开始的。我在本地装好 Python 环境把 backtrader 跑起来看着自选品种在历史数据上画出一条漂亮的净值曲线那一刻确实有点飘。但真正从 0 到 1 跑通实盘我前后折腾了大半年把回测里没出现过的坑全踩了一遍行情断流、订单状态不一致、滑点吞噬利润、盘中想手动干预甚至还有一次因为数据没复权策略在除权日发出了夸张的买入信号。后来我把整个流程收敛成三步法回测建模、模拟盘验证、小资金实盘。每一步都有明确的验收标准和退出条件别想跳过中间那步直接上战场。这篇内容就写给那些同样想从回测走向实盘的程序员我把每一步怎么设计、关键代码怎么改、最容易出问题的地方在哪全部按实操顺序讲一遍。1. 三步法整体思路为什么不能直接从回测跳到实盘1.1 回测、模拟盘、实盘分别解决什么问题很多程序员把量化交易理解成一套算法题设计好策略喂数据导出收益曲线然后就能躺着赚钱了。我一开始也这么想结果回测漂亮得像印钞机模拟盘一跑就露馅实盘更是连续打脸。后来我才明白量化交易本质上是一套分布式系统回测只是在验证策略逻辑模拟盘在验证实时链路实盘则验证资金和情绪的承受能力。回测解决的是“这个策略在历史上是否有效”它的核心输入是历史数据核心输出是净值曲线、回撤、胜率和盈亏比。模拟盘解决的是“实时行情下信号能不能及时产生、订单能不能顺利成交”它的核心是数据链路和订单状态管理。实盘解决的是“真实交易环境里策略是否还能保持稳定”它牵扯到滑点、手续费、接口稳定性、资金管理和最容易被忽视的执行纪律。三步之间是层层递进的关系跳过任何一步都会漏掉最关键的系统性问题。实盘阶段暴露的很多问题往往根本不是策略问题而是工程问题。1.2 三段时间与资源怎么分配我见过不少人回测只跑了两天就急着开实盘账户结果三个月亏光本金然后得出结论“量化是骗人的”。这不是量化的问题是流程的问题。按我的经验回测阶段至少占用整个项目 60% 的时间包括策略设计、数据清洗、参数验证、鲁棒性分析。模拟盘阶段占用两到四周重点测试行情适配、订单通道和系统稳定性。实盘阶段第一笔资金一定要小建议掌握在个人总可用资金的 5% 到 10%先跑一两个月验证一致性稳定后再逐步加仓。这样分配时间会让整个进度看起来非常慢但恰恰是“慢”在保护你。回测阶段多花时间是为了减少实盘阶段的意外模拟盘阶段多花时间是为了验证链路而不是为了等一个收益结果。实盘阶段用小资金是为了在可控风险内补齐最后一个未知变量真实市场里的流动性和交易者行为。1.3 哪些程序员适合走这套三步法说句实在话量化交易并不要求你是顶级算法工程师但需要具备三样基础能力Python 能熟练写脚本熟悉基本的数据清洗和 API 调用并且有耐心查日志定位问题。如果你做过订单系统、消息队列或者任何带状态机的后端系统那更合适。量化系统真正的难点从来不是策略公式而是“数据、信号、成交”三者保持一致这本质上就是数据一致性问题。如果你目前是刚入门的程序员我的建议是先从回测框架入手不要上来就研究复杂模型。把一次简单的双均线策略完整走完三步收获会比研究一年深度学习模型大得多。这套三步法的核心是用工程方法控制不确定性而不是用复杂模型预测市场。2. 第一步用回测框架先跑通历史数据2.1 框架选型为什么我选了 backtrader 而不是自己造轮子回测框架的选择我见过两种极端一种是什么都自己写觉得这样最可控另一种是无脑套用平台根本搞不清内部逻辑。我自己用下来的结论是用 backtrader 这样的开源框架做骨架再按需扩展效率最高。原因主要有三点第一backtrader 的数据加载、指标计算、交易撮合已经很成熟不需要重复造数据对齐、滑点模拟、头寸管理这些轮子。第二它的设计是事件驱动的内置 data feed 和 broker 抽象天然方便从回测切换到模拟盘甚至实盘。第三社区足够大网上资料多遇到问题几乎都能搜到答案这点对刚开始接触量化的人特别友好。如果你只是做日线级别的指数轮动用纯 pandas 写回测也确实够用几十行代码就能跑通。但一旦涉及到分钟级数据、多标的组合、复杂订单类型自己写撮合逻辑会非常痛苦。我的习惯是先用 backtrader 快速验证策略逻辑逻辑跑通后再把性能敏感的部分比如 K 线数据处理、批量指标计算用 numpy 或 numba 重写。这样既有开发速度又有运行性能。2.2 数据准备数据质量直接决定回测质量数据是整个回测的地基这里也是我踩过最大坑的地方。刚开始我用下载的免费数据直接跑没有做任何清洗csv 里缺列、重复时间戳、不复权的问题全部存在回测结果被虚高得离谱。后来我养成一个固定习惯无论数据来自哪里入库前必须做三件事检查时间序列是否连续有没有跳变的缺口查看时间戳是否有重复有则排序后去重确认复权方式。复权这一点特别容易出问题。回测时如果使用后复权数据策略信号整体更接近真实历史表现用前复权最新一段价格相对准确但早期价格被大幅调整任何依赖绝对价格阈值的策略都会被干扰。我遇到过最典型的一个案例策略里写了“价格低于 3 元才买入”的过滤条件结果用了前复权数据一支股票的历史价格被调整到 1 块多直接触发了大量虚假买入信号回测年化瞬间被拉高。另一个容易被忽略的点是分钟级数据的时区对齐。我在跑国内期货和股票的分钟数据时经常因为本地时间和交易所时间不一致导致 9:30 开盘后的第一根 bar 对不上。后来统一用交易所所在时区作为所有数据的基准外部数据入库前先转换时间这个问题才算彻底解决。数据如果不一致回测跑得再快也没有任何意义因为结果本身就是错的。注意数据清洗不是回测的“前置步骤”而是回测的一部分。数据如果不准后面所有策略优化都是在错误土地上盖楼越盖越危险。2.3 一个能直接跑起来的回测骨架下面是一个用 backtrader 做简单均线策略回测的骨架代码包含了数据加载、佣金设置、仓位控制和回撤分析适合作为第一版模板。import backtrader as bt class SmaCross(bt.Strategy): def __init__(self): self.sma_fast bt.ind.SMA(period10) self.sma_slow bt.ind.SMA(period30) def next(self): if not self.position: if self.sma_fast[0] self.sma_slow[0]: self.buy() elif self.sma_fast[0] self.sma_slow[0]: self.close() cerebro bt.Cerebro() data bt.feeds.GenericCSVData( datanamedaily.csv, dtformat%Y-%m-%d, openinterest-1, ) cerebro.adddata(data) cerebro.broker.setcash(100000.0) cerebro.broker.setcommission(commission0.001) cerebro.addsizer(bt.sizers.PercentSizer, percents10) cerebro.addanalyzer(bt.analyzers.Returns, _namereturns) cerebro.addanalyzer(bt.analyzers.DrawDown, _namedd) cerebro.addanalyzer(bt.analyzers.SharpeRatio, _namesharpe) result cerebro.run() strat result[0] print(收益率:, strat.analyzers.returns.get_analysis()[rnorm]) print(最大回撤:, strat.analyzers.dd.get_analysis()[max][drawdown]) print(夏普比率:, strat.analyzers.sharpe.get_analysis()[sharperatio])这段代码虽然简单但已经把回测阶段最核心的几个分析器加上了。我特别建议把收益、最大回撤、夏普比率三件套固定下来每次策略调整后都看这三个指标不要只盯收益率。很多人看到年化 50% 就兴奋但最大回撤 40% 的策略在实盘里根本拿不住净值曲线从高点跌下来的时候你的手会抖你的逻辑会乱最后大概率在最低点附近止损离场。2.4 回测阶段新手最容易忽略的细节回测阶段的验收标准我从来不是看“收益多高”而是看“回撤多深”和“参数是否敏感”。除了 2.3 代码里的三个分析器我还会手动检查几个维度盈亏比和胜率的组合是否合理样本内和样本外表现差异是否过大以及滑点和手续费是否能真实地穿透到每笔交易里。关于滑点和手续费很多回测框架默认是 0你必须手动加上。我一般会给股票加 0.1% 到 0.2% 的综合成本期货按每手固定费用加 1 到 2 个价格跳动的滑点。这样做出来的净值曲线才会真实。还有一个细节是仓位控制使用 PercentSizer按账户净值比例下单比固定手数更贴合真实资金增长曲线。回测到后期还要做“参数鲁棒性”检验。简单说就是把策略参数在合理范围内微调比如均线周期从 10 改成 8 和 12看策略表现是否剧烈波动。如果参数一变动收益从年化 40% 直接变成亏损那说明策略只是在这段历史数据上被“硬拟合”了后面实盘一定会失效。期货放大杠杆的交易者尤其要注意这一点因为参数敏感策略的崩溃速度比你想象中快得多。3. 第二步把 backtrader 从回测扩展到模拟盘3.1 模拟盘不是接个实时行情就行回测时你的程序读的是历史 csv 文件每一根 K 线都是确定的策略在 bar 收盘后运行天然不会用到未来数据。但到了模拟盘行情是实时推过来的策略运行在一根尚未完成的 bar 上信号会不会反复横跳、订单能不能成交、成交回报什么时候回来这些问题全都变成了不确定性。模拟盘要解决的核心问题是“数据、信号、成交”的一致性。策略在 10:15 产生了买入信号你在 10:16 下单实际成交价已经比信号价高了两跳这很正常。但如果系统因为网络延迟把 10:15 的旧行情当成最新行情去产生信号那就不是滑点是故障。所以我常说模拟盘阶段重点不是看收益率而是验证“信号有没有迟到、订单有没有漏单、成交回报有没有吃进系统”。3.2 扩展 backtrader 需要改哪些代码如果你也用 backtrader从回测到模拟盘核心改动是两部分数据源和订单执行通道。数据源方面需要把原来读 csv 的bt.feeds.GenericCSVData替换成自己实现的数据类数据来源换成实时行情接口。bt.feeds.DataBase是基类你需要实现_load方法每次有新 bar 时返回 True没有新数据时返回 False。数据可以轮询拉取也可以订阅推送关键是要保证策略每次触发next时拿到的 bar 是完整的、是最新的。订单执行方面回测时成交几乎瞬时完成默认不考虑排队延迟。模拟盘阶段可以继续用 backtrader 自带 broker 做“本地撮合”行情用实时数据这样能快速验证策略逻辑在实时行情下是否正常也可以接券商或期货公司的仿真交易接口订单真的发到对方柜台但不涉及真实资金这样能验证网络链路、订单状态管理、撤单重试等真实环节。我的建议是两步都走先本地撮合验证逻辑再仿真接口验证通道。3.3 代码示例实时数据源和订单状态日志下面我给出一个扩展方向上的示例代码帮你理解改造思路。import backtrader as bt class RealTimeFeed(bt.feed.DataBase): def __init__(self, fetch_bar_func): super().__init__() self.fetch_bar_func fetch_bar_func self._last_ts 0 def _load(self): bar self.fetch_bar_func() if bar is None: return False ts, open_, high, low, close, volume bar if ts self._last_ts: return False self.lines.datetime[0] ts self.lines.open[0] open_ self.lines.high[0] high self.lines.low[0] low self.lines.close[0] close self.lines.volume[0] volume self._last_ts ts return True这段代码解决的是数据源接入问题。fetch_bar_func是从行情接口拉取最新 bar 的函数_load被 backtrader 反复调用有新数据就填进 lines 并返回 True。注意ts self._last_ts的判断这是为了防止重复时间戳导致同一根 bar 被多次消费。策略侧还需要加一个订单状态日志我建议在notify_order里把每个订单的状态变化完整记录下来。def notify_order(self, order): if order.status order.Submitted: self.log(订单已提交) elif order.status order.Accepted: self.log(订单已接受) elif order.status order.Completed: self.log(f成交 价格{order.executed.price} 数量{order.executed.size}) elif order.status order.Canceled: self.log(订单已撤销) elif order.status order.Margin: self.log(订单保证金不足)这段日志在回测阶段看起来可有可无但到了模拟盘和实盘它就是排障的第一手证据。我遇到过很多次系统看起来“什么都没干”实际是订单被拒单或者保证金不足没有日志根本定位不到原因。3.4 模拟盘阶段的验收标准模拟盘不能一直挂在那边看净值需要设定明确的验收标准。我自己常用的三个指标第一模拟盘和回测在同时间段的收益曲线相关性用来判断策略在真实行情下是否走样第二订单成交率和滑点统计如果信号触发 10 次只有 3 次成交说明策略对流动性过于敏感第三系统连续运行时间是否达到一周以上有没有内存增长、卡死、重连失败这些问题。模拟盘要跑多久取决于策略的交易频率。日线策略跑一两个月已经足够覆盖足够多的信号样本分钟级策略可能跑一周就能暴露大部分链路问题。核心原则是“覆盖足够多的交易信号”而不是单纯按时间长度做判断。4. 第三步实盘跑通的工程化细节4.1 实盘前必须完成的 5 项系统检查第一次上实盘前我列了一张检查清单后来每次上线新策略都会重新过一遍。这五项检查缺一不可。第一资金准备。第一笔资金小到即使全部亏损也不影响生活这是底线。我建议控制在总可用资金的 5% 到 10%再多就不建议了。第二接口权限。确认实盘 API 和模拟盘接口是否一致是否有额外的权限申请门槛比如期货程序化交易需要交易所报备股票接口可能需要特定券商的权限。第三交易规则确认。最小交易单位、涨跌停限制、T1 制度、手续费率这些必须和回测模型里的设置保持一致。我见过有人回测里做了当日买入当日卖出实盘才发现股票是 T1策略瞬间变成另一个策略。第四风控模块。单笔最大亏损、单日最大回撤、总仓位上限这些一旦触发系统必须能自动停止开仓。这里我强烈建议把风控逻辑独立在策略之外单独做成一个守护模块。第五故障预案。程序崩溃了怎么办API 断线重连逻辑是否健全本地机器重启后系统能否自动恢复这些问题必须提前考虑不能等到盘中才想。4.2 交易接口对接与订单状态机的核心不变量对接实盘接口不同市场的 API 差别很大但核心链路是一致的拿行情 - 策略产生信号 - 生成订单 - 通过接口发送 - 接收成交回报 - 更新持仓。以期货 CTP 接口为例程序化交易必须处理订阅行情、报单、成交回报这些环节每一环都是独立的异步回调。实盘中最容易出问题的环节是“订单状态不一致”。比如你发送了一个限价单接口显示已报但交易所成交回报迟迟不来你无法确定订单到底成交了没有。如果不处理这种状态就可能重复下单或者漏单。我的经验是在订单状态机里加入超时和“撤销后重新确认”的逻辑超过一定时间未收到明确回报主动发送撤单请求根据撤单结果重新确认状态。这里最重要的一个不变量是在确认前一笔订单终结之前绝不发送下一笔同方向订单。很多程序化的重大事故都是因为旧订单没有确认完成系统又发了新订单导致仓位被叠加放大。把这个不变量写进代码就是一道硬性的安全边界。4.3 资金管理与仓位计算程序员最喜欢找最优解但资金管理绝对不是找最优解而是找“最不坏”的解。我的经验是单笔风险控制在总资金的 0.5% 到 1% 以内单日风险控制在 2% 到 3% 以内。为什么这么保守因为量化策略的亏损很少是均匀展开的它经常是集中出现、连续出现。连续止损 10 次如果单笔风险控制在 1%总回撤也才 9.5%还在可承受范围内你还有足够的信心让策略继续跑下去。仓位计算上我一直使用基于账户当前权益的比例法而不是固定手数。公式很简单计划仓位比例等于单笔风险预算除以止损价格距离。举例来说账户权益 100 万策略规定单笔亏损不超过 1%也就是 1 万元标的的止损距离是 2%那计划仓位就是 1% / 2% 50%。接下来再考虑保证金比例、流动性、单票集中度上限最后再打折执行。这个公式虽然简单但效果很好它能让你在资金曲线增长时线性加仓在回撤时自动缩小仓位。配合动态权益计算天然形成了一种“赢冲输缩”的状态比固定手数抗回撤能力强得多。4.4 实盘监控写日志和告警比盯行情更重要实盘上线后不代表你可以放手不管。我建议第一周每天收盘后检查一次日志对比“策略信号记录”和“实际成交记录”的偏差。后来我写了一个简单的监控脚本每天定时拉取账户权益、持仓数量、当日成交笔数、当日盈亏然后汇总成一份报告推送到手机。我用的监控思路很简单一个独立于策略的守护进程每隔几秒检查一次行情源心跳超过 N 秒没有新 bar就标记行情异常并暂停开仓盘中每天实时计算账户权益和浮动盈亏一旦单日亏损触发阈值立即发送告警并触发“只平仓不开仓”的风控状态。这里要特别注意我不建议追求“完全自动化到不用管”。实盘总会遇到边界情况比如合约换月、停牌复牌、涨跌停封板这些场景很难用代码穷举。我把系统设计成了半自动模式信号由策略自动生成但关键风控动作比如清仓、暂停策略由人工确认。这样既降低了情绪干扰又保留了人的判断力。程序员的优势是能写工具但工具的最终目的不是取代你的判断而是让你在信息完整的情况下做判断。5. 常见问题与排查技巧实录5.1 回测表现很好模拟盘却连续亏损这种情况最常见的两个原因一个是过拟合一个是数据频率不一致。过拟合就是回测阶段反复优化参数让策略在这段历史数据上表现得太“完美”真实市场一到立即失效。排查方法也简单做样本外测试把 2018 年到 2021 年的数据做训练集2022 年到 2024 年做检验集如果样本外收益明显下降说明策略的“聪明”来自过度优化。另一个原因是数据频率不一致。回测用的分钟数据模拟盘因为数据源或时区问题实际拿到的是不同步的 bar信号自然对不上。排查方法是对比模拟盘的信号日志和回测的同时间段信号看偏差出在数据环节还是参数环节。5.2 模拟盘一切正常实盘却频繁不成交这在限价单策略里特别常见。回测和模拟盘默认价格到了就成交但真实市场里你的单子要排在队列后面价格到了也不一定轮得到你。解决思路有三个第一改用对手价下单尽快成交但它会承担一定滑点第二拆单把大单拆成几个小单逐步成交第三在策略层面接受滑点成本把滑点预期写进回测参数。我在实盘初期遇到过 10 次信号只成交 2 次的情况后来检查才发现LIMIT 单挂得太远离最新价明明离成交只差一个价位但那个价位在盘口根本没有出现过。这种情况可以通过观察盘口深度来优化下单价格不一定非要追单但至少要让价格贴近真实成交区间。5.3 行情中断、重连后出现虚假信号实盘中最怕的是行情源断流。策略还在正常计算信号但信号基于的数据已经陈旧了。我的做法是在行情接口外面加心跳检测如果超过 N 秒没有收到新 bar立即暂停新开仓并发出告警。程序在重连后不能马上恢复交易要先做数据对齐确认断开期间缺失的数据已经被合理标记或补齐才能继续运行。这里容易藏着一个隐藏 bug行情断流期间某些数据源会返回一个“补全”的旧 bar系统自动恢复后策略把这个旧 bar 当成新行情处理发出错误的追单指令。所以重连后的第一件事不应该是开仓而应该是“静默观察”若干根 bar让策略的数据流回到正常节奏。5.4 常见问题排查速查表为了便于查阅我把上面几个问题整理成一个表格。现象可能原因排查方法处理建议回测很好模拟盘亏损过拟合、参数敏感样本外测试、参数区间扫描简化策略减少优化次数模拟盘稳定实盘不成交限价单挂价不合理查看盘口深度和成交回报改用对手价或拆单行情断流后误发信号缺少心跳检测检查行情时间戳连续性暂停开仓静默观察后再恢复重复下单或漏单订单状态机不完整检查订单日志状态变化前一单未终结不发下一单盘中想手动干预情绪干扰记录干预理由并对照规则规则外一律不执行5.5 一点关于手动干预的额外经验最后补一条不是技术、但比技术更容易引发亏损的经验想干预时先写下一段理由然后对照原有策略规则判断这段理由是否在规则框架内不在框架内就不执行。我见过太多人策略本身没问题但盘中看到浮盈就想加仓看到浮亏就想止损最后完全不按系统规则操作。程序员的优势是逻辑性强这反而应该是执行纪律最强的优势而不是靠盘感去抢短线。6. 一些实在的建议给准备走这三步的人说几句掏心窝的话。我看到不少程序员进入量化投资第一反应是研究复杂模型神经网络、强化学习轮番上阵结果回测一塌糊涂。我自己也走过这段弯路最后发现能稳定盈利的策略往往是那些逻辑简单、容量足够、执行纪律清楚的策略。三步法的核心精神不是教你如何预测市场而是教你如何用工程化思维管理不确定性。如果你是从零开始我建议先按第 2 章的内容搭一个基于 backtrader 的日线级别回测选择一两个你熟悉的标的不求高收益先把“数据、信号、成交”这条链路走通。链路走通之后再进入第 3 章的模拟盘。这个过程中写日志的习惯一定要从第一天开始养成因为后期排查问题日志就是你的唯一依据。我个人这几年的体会是做量化投资最难的其实不是策略而是接受“你不可能控制市场只能控制自己的风险”。程序代码是完全可以靠逻辑验证的但市场的随机性永远在那里。把每一步都走扎实先求不亏钱、不失控再谈赚钱这条路才是真正能从 0 到 1 的路径。回测里的高收益只是漂亮的起点实盘里那个能稳定运行、不出事故的系统才是你真正的资产。
返回列表