我们重新运行了一份已保存的回测,得到的权益曲线却不一样。策略代码相同,日期范围相同,交易品种也相同。最终余额相差了 1.8%,还有 3 笔交易错开了 1 根 K 线。
这足以让人难以信任研究结果。如果队友、未来的你,或模拟交易服务无法复现这次运行,你就无法判断究竟是策略改进了,还是实验本身变了。下面是我们追查偏差的过程。
第 1 天:我们明确了“同一次运行”的定义
我们最初的错误,是把策略文件当成了整个实验。事实并非如此。这次运行还依赖输入数据、引擎版本、交易日历、合约元数据和执行设置。代码只描述了计算过程的一部分。
在改动任何内容之前,我们先制作了一份运行清单,记录策略提交版本、数据快照标识符、日期范围、交易场所、费率表、资金费率来源、成交模型和软件版本。我们还保存了生成的订单和成交记录,因为单看权益曲线无法判断两次运行最早从哪里开始分歧。
| 产物 | 记录内容 | 重要原因 |
|---|---|---|
| 市场数据 | 快照 ID、架构版本、调整方式 | 数据供应商会修复历史数据并修订公司行动信息 |
| 执行 | 费率等级、资金费率序列、成交和冲击设置 | 默认值和账户假设会改变结果 |
| 运行环境 | 代码提交版本、引擎和依赖版本 | 库的变化可能影响排序、舍入或指标计算 |
| 输出 | 订单、成交、持仓和指标 | 用于定位两次运行开始出现分歧的位置 |
第 2 天:我们比较了交易,而不是 Sharpe
汇总指标分散了我们的注意力。两次运行的 Sharpe 几乎相同,但成交日志显示,第一次不匹配出现在资金费率结算时。一次运行把费率计入结算时间戳时持有的仓位;另一次则按该时间戳再平衡后的仓位计算。
策略代码没有变化,变化的是引擎的事件顺序。一次小版本更新明确了事件顺序,而此前顺序取决于两个事件碰巧如何排序。
我们在运行约定中明确了顺序:先对结算时持有的仓位应用资金费率,再处理该时间戳的策略决策。具体约定可能因交易场所和引擎而异。把顺序留作隐含设置才是问题所在。
第 3 天:所谓“相同”的数据文件其实不同
固定事件顺序后,剩余的不匹配集中在几笔股票交易上。数据供应商修正了历史拆股调整。我们的文件名称和行数都与之前相同,因此看起来没有变化。
现在,我们会为每个不可变数据快照生成指纹,并将调整规则与其一并保存。哈希值能告诉我们字节是否发生变化,却无法解释原因。因此,清单还会记录来源、获取时间和转换版本。对于会被修订的数据,这些细节也是结果的一部分。
要让回测可复现,就得回答“它看到的是哪个版本的过去?”
第 4 天:我们发现了一个不起眼的默认值
最后一处差异来自 maker 费率:由于策略配置中漏掉了这个字段,它被设为 0。较新版本的引擎应用了账户默认费率。单是这个默认值就足以改变边际交易,解释了最终余额差距的大部分。
我们把具有实际经济影响的设置都明确写出,并让引擎将解析后的配置打印到运行记录中。探索阶段使用默认值很方便;但在跨时间比较结果时,它们算不上可靠证据。
下次我们会跳过什么
我们花了半天比较汇总指标,却没先检查第一笔不同的成交。别从这里开始。按时间戳对两份事件日志排序,找出最早的分歧;之后的差异往往都是由同一个原因引起的。
我们也不会再以为,仅靠容器镜像就能让运行可复现。它能固定大部分软件环境,却无法固定外部数据文件、运行时获取的费率表,或数据供应商修订后的历史记录。
回测结果发生变化时,保存两次运行的清单和日志,然后逐一排查偏差来源。真正有用的结果,不只是能重新跑出同一条曲线,还要有记录说明哪些数据和假设生成了它,以及下次运行为何可能不同。
← 全部文章


