2026年九月26日 · 研究

我们尝试复现一项策略的回测,结果发现数字从这里开始偏离。

我们尝试复现一项策略的回测,结果发现数字从这里开始偏离。

我们重新运行了一份已保存的回测,得到的权益曲线却不一样。策略代码相同,日期范围相同,交易品种也相同。最终余额相差了 1.8%,还有 3 笔交易错开了 1 根 K 线。

这足以让人难以信任研究结果。如果队友、未来的你,或模拟交易服务无法复现这次运行,你就无法判断究竟是策略改进了,还是实验本身变了。下面是我们追查偏差的过程。

第 1 天:我们明确了“同一次运行”的定义

我们最初的错误,是把策略文件当成了整个实验。事实并非如此。这次运行还依赖输入数据、引擎版本、交易日历、合约元数据和执行设置。代码只描述了计算过程的一部分。

在改动任何内容之前,我们先制作了一份运行清单,记录策略提交版本、数据快照标识符、日期范围、交易场所、费率表、资金费率来源、成交模型和软件版本。我们还保存了生成的订单和成交记录,因为单看权益曲线无法判断两次运行最早从哪里开始分歧。

产物记录内容重要原因
市场数据快照 ID、架构版本、调整方式数据供应商会修复历史数据并修订公司行动信息
执行费率等级、资金费率序列、成交和冲击设置默认值和账户假设会改变结果
运行环境代码提交版本、引擎和依赖版本库的变化可能影响排序、舍入或指标计算
输出订单、成交、持仓和指标用于定位两次运行开始出现分歧的位置

第 2 天:我们比较了交易,而不是 Sharpe

汇总指标分散了我们的注意力。两次运行的 Sharpe 几乎相同,但成交日志显示,第一次不匹配出现在资金费率结算时。一次运行把费率计入结算时间戳时持有的仓位;另一次则按该时间戳再平衡后的仓位计算。

策略代码没有变化,变化的是引擎的事件顺序。一次小版本更新明确了事件顺序,而此前顺序取决于两个事件碰巧如何排序。

我们在运行约定中明确了顺序:先对结算时持有的仓位应用资金费率,再处理该时间戳的策略决策。具体约定可能因交易场所和引擎而异。把顺序留作隐含设置才是问题所在。

第 3 天:所谓“相同”的数据文件其实不同

固定事件顺序后,剩余的不匹配集中在几笔股票交易上。数据供应商修正了历史拆股调整。我们的文件名称和行数都与之前相同,因此看起来没有变化。

现在,我们会为每个不可变数据快照生成指纹,并将调整规则与其一并保存。哈希值能告诉我们字节是否发生变化,却无法解释原因。因此,清单还会记录来源、获取时间和转换版本。对于会被修订的数据,这些细节也是结果的一部分。

要让回测可复现,就得回答“它看到的是哪个版本的过去?”

第 4 天:我们发现了一个不起眼的默认值

最后一处差异来自 maker 费率:由于策略配置中漏掉了这个字段,它被设为 0。较新版本的引擎应用了账户默认费率。单是这个默认值就足以改变边际交易,解释了最终余额差距的大部分。

我们把具有实际经济影响的设置都明确写出,并让引擎将解析后的配置打印到运行记录中。探索阶段使用默认值很方便;但在跨时间比较结果时,它们算不上可靠证据。

3个偏差来源
1.8%最初的最终余额差异
0仅凭 Sharpe 相同所能说明的价值

下次我们会跳过什么

我们花了半天比较汇总指标,却没先检查第一笔不同的成交。别从这里开始。按时间戳对两份事件日志排序,找出最早的分歧;之后的差异往往都是由同一个原因引起的。

我们也不会再以为,仅靠容器镜像就能让运行可复现。它能固定大部分软件环境,却无法固定外部数据文件、运行时获取的费率表,或数据供应商修订后的历史记录。

回测结果发生变化时,保存两次运行的清单和日志,然后逐一排查偏差来源。真正有用的结果,不只是能重新跑出同一条曲线,还要有记录说明哪些数据和假设生成了它,以及下次运行为何可能不同。

可复现性回测数据工程模拟交易
← 全部文章