这篇文章讲我系统里一次真实的"跑批事故":walk-forward 验证的代码改了重跑,结果和基准差出一大截(219 笔 vs 272 笔成交)——差点把一批实验结论全部污染。整个过程里有三个可复制的动作:先对账、再冻结、后硬校验。每步都配真实数字和代码。
同一个策略、同一段行情、同一份数据(2026-02-02 ~ 2026-09-30,161 个交易日):
样本内收益是样本外的六倍多、回撤只有一半。如果这个差距不动摇你"拿样本内报数"的冲动,那后面的事故也吓不到你——真正的问题在于:test 数字一旦被污染,你自己都不会知道。
背景:我给执行引擎做了一次内核升级(profile 驱动,v2)。升级后按纪律要求"重跑 walk-forward 并与旧基准逐值比对"。第一次比对结果:
53 笔的缺口。第一反应是"v2 内核有 bug"。但止损点在这里:我没有直接去改 v2 内核,而是先把两边产物逐日拉齐对比——发现差异从第一天就出现,且集中在"体制分支"上:本该走"弱市分支"的日子走了"强市分支"。
顺着分支查下去,根因浮出水面:重跑脚本漏调了一个预处理步骤(体制图预计算 prepare_regime)。体制图不预计算,分支判定就退化为常量 → 所有日期的强弱判断全错 → 选出的候选自然对不上(219 vs 272)。
这里最值得记的不是 bug 本身,而是:如果没有"重跑必须与基准逐值比对"这条规矩,219 笔的版本会安安静静地成为新一版"结论"——毕竟它不报错、不崩溃、数字看起来也合理。
修掉漏调之后,我做了三件事把这类事故钉死:
1. 对账阈值写进代码:重跑完成自动与基准比对,笔数全等 + 收益/回撤偏差 ≤ 0.01%(写死,不可配置):
# 重跑产物 vs 基准:不一致即拒绝入库(而不是写 warning) assert new["trade_count"] == base["trade_count"] assert abs(new["return_pct"] - base["return_pct"]) <= 0.01 assert abs(new["max_drawdown_pct"] - base["max_drawdown_pct"]) <= 0.01
2. 失败即拒绝入链:校验不通过,产物不入证据链(写不进实验台账,也就不可能被后续报告引用)——"错了但要先发出去再说"在结构上不可能发生。
3. 内核版本分账:重跑产物标注所用内核版本(这里是 profile-exec-v2.0.0),与旧基准的版本并列可查——"哪个数字出自哪个引擎"永远可回答。
修复后的最终对账:+133.805737% / −25.930672% / 272 笔,逐值精确到 1e-6——比"看起来一样"强得多,因为它是可断言的。
无论你用什么回测框架,这三件事值得照抄:
1. 先对账,再归因:数字对不上时,先拉齐两边产物找第一个分叉点,不要直接改代码——这次如果先改了内核,大概率引入第二个 bug,而且再也不知道原来那个对不对。
2. 重跑必须有基准:任何"代码变更后重跑",都要有旧产物作为基准硬校验(笔数全等是最便宜的第一道)。没有基准的重跑 = 没有验收。
3. 校验不通过就不许流动:产物要么带全部校验通过标记入库,要么被拒绝——不存在"带瑕疵流转,后面注意点"。
1. 切数据一刀两段:train 在前、test 在后,不重叠;
2. 参数只在 train 上定,test 跑完之前不改任何东西;
3. 想调参?作废本次验证,从 test 之后的新数据重开一段;
4. 三段数字(train / test / 全期)并列汇报,回撤与收益一起看;
5. 试验基数 N 如实计数,失败的实验也记录在案——防止"多试几次凑出一个好结果"。
本站内容为个人研究记录与方法分享,模拟,不构成投资建议;据此操作风险自负。
Joyous 的技术笔记 · 沪ICP备2026053770号
隐私最小化:不设 cookie、无表单、无第三方脚本;访问统计仅取服务端日志(≤30 天,仅聚合),不导出 IP。