回测、模拟盘和实盘都能正常运行,不代表它们执行的是同一套策略。Alpha-Online 的一次事故中,三条路径分别实现了信号生成、风控和因子筛选。每条路径单独看都“有道理”,合起来却无法复现彼此的结果。

问题不在于少写了几个测试,而在于系统允许同一条交易决策存在三份真相。

三份实现为什么一定会漂移

假设最初三条路径完全一样。随着系统迭代,下面任何一个小改动都可能只落到其中一处:

  • 信号从当日数据改为上一交易日数据;
  • Top 10 的并列排名采用不同处理;
  • 单票仓位上限从 10% 调为 8%;
  • 缺失因子值从剔除改为填零;
  • 目标股数的取整规则不同;
  • 止损先于新信号还是后于新信号执行。

这些差异大多不会抛异常。程序继续输出持仓和净值,只是三条路径越来越像三套策略。

先定义什么必须唯一

One Codebase 不是要求所有代码都塞进一个文件,而是要求产生交易决策的核心逻辑只有一个实现

可以把系统拆成输入、决策与执行三层:

历史数据 ─┐
实时数据 ─┼─> 标准化数据 -> SignalGenerator -> RiskManager -> 目标持仓
模拟数据 ─┘                                      |
                                      回测成交 / 模拟下单 / 实盘下单

允许变化的是数据适配器和成交适配器;不应复制的是从标准化数据到目标持仓的决策链。

在这条链中,至少以下部分必须共享:

  1. 数据清洗与缺失值规则;
  2. 因子计算和信号时点;
  3. 截面排名、Top N 与权重;
  4. 风控约束与目标持仓裁剪;
  5. 调仓日历与非调仓日行为。

怎样证明回测和实盘是同一套策略

“调用了同一个类”仍然不够。更可靠的做法是建立可逐层比对的决策快照。

固定输入做黄金样本

选取一个规模足够小、能够人工检查的历史截面,保存:

  • 清洗后的输入字段;
  • 每个因子的原始值与排名;
  • 筛选后的候选列表;
  • 风控前后的目标权重;
  • 取整后的目标股数。

让回测入口和模拟/实盘入口读取同一份快照,在不真正下单的模式下输出目标持仓。每一层都应一致,而不是只比较最后收益。

给决策结果加指纹

对策略版本、参数、数据截止时间、股票池版本和目标持仓生成可追溯指纹。发生差异时,可以先回答“输入不同、版本不同,还是决策不同”,避免把所有问题都归为市场噪声。

把时间隔离写成契约

历史回测必须显式保证第 tt 日决策只使用第 t1t-1 日及更早数据;实时路径则在数据真正可得后才运行。两边的实现方式可以不同,但信息集合必须相同。

如果回测通过 shift(1) 隔离未来,模拟盘却在收盘前使用尚未完成的当日数据,它们仍然不是同一策略。

常见的伪修复

只让三份代码“看起来一样”

复制粘贴后逐行对齐只能解决当前差异,不能阻止下一次漂移。只要仍有三份实现,维护成本就会重新制造分叉。

只比较最终净值

净值相近可能只是误差互相抵消,净值不同也无法指出是哪一层造成。目标持仓、换手与订单路径比最终曲线更适合定位。

用更多测试保护重复实现

测试可以发现差异,却无法消除重复真相。优先收敛架构,再用测试守住唯一实现。

一份可执行的收敛清单

  1. 列出回测、模拟盘、实盘所有决策入口;
  2. 标记重复的清洗、因子、筛选、风控和调仓代码;
  3. 选定唯一的 SignalGeneratorRiskManager
  4. 让三条路径只提供标准化输入,并消费同一目标持仓输出;
  5. 建立固定输入的逐层对照测试;
  6. 在日志中记录数据截止时间、参数和策略版本;
  7. 删除旧实现,避免“暂时兼容”变成永久分叉。

从架构问题回到研究问题

只有当回测和执行共享同一条决策路径,回测指标才有资格参与实盘判断。否则,所谓“实盘验证回测”只是拿一套系统的结果解释另一套系统的行为。

One Codebase 的价值不是代码更整洁,而是让失败可以归因:输入、信号、风控、成交或市场变化,每一层都有可复核边界。

如果你的系统已经共享决策代码,但仍不确定因子公式本身在表达什么,可以回到价量因子怎么读,从输入、排名和方向三层建立阅读方法。