回测、模拟盘和实盘都能正常运行,不代表它们执行的是同一套策略。Alpha-Online 的一次事故中,三条路径分别实现了信号生成、风控和因子筛选。每条路径单独看都“有道理”,合起来却无法复现彼此的结果。
问题不在于少写了几个测试,而在于系统允许同一条交易决策存在三份真相。
三份实现为什么一定会漂移
假设最初三条路径完全一样。随着系统迭代,下面任何一个小改动都可能只落到其中一处:
- 信号从当日数据改为上一交易日数据;
- Top 10 的并列排名采用不同处理;
- 单票仓位上限从 10% 调为 8%;
- 缺失因子值从剔除改为填零;
- 目标股数的取整规则不同;
- 止损先于新信号还是后于新信号执行。
这些差异大多不会抛异常。程序继续输出持仓和净值,只是三条路径越来越像三套策略。
先定义什么必须唯一
One Codebase 不是要求所有代码都塞进一个文件,而是要求产生交易决策的核心逻辑只有一个实现。
可以把系统拆成输入、决策与执行三层:
历史数据 ─┐
实时数据 ─┼─> 标准化数据 -> SignalGenerator -> RiskManager -> 目标持仓
模拟数据 ─┘ |
回测成交 / 模拟下单 / 实盘下单
允许变化的是数据适配器和成交适配器;不应复制的是从标准化数据到目标持仓的决策链。
在这条链中,至少以下部分必须共享:
- 数据清洗与缺失值规则;
- 因子计算和信号时点;
- 截面排名、Top N 与权重;
- 风控约束与目标持仓裁剪;
- 调仓日历与非调仓日行为。
怎样证明回测和实盘是同一套策略
“调用了同一个类”仍然不够。更可靠的做法是建立可逐层比对的决策快照。
固定输入做黄金样本
选取一个规模足够小、能够人工检查的历史截面,保存:
- 清洗后的输入字段;
- 每个因子的原始值与排名;
- 筛选后的候选列表;
- 风控前后的目标权重;
- 取整后的目标股数。
让回测入口和模拟/实盘入口读取同一份快照,在不真正下单的模式下输出目标持仓。每一层都应一致,而不是只比较最后收益。
给决策结果加指纹
对策略版本、参数、数据截止时间、股票池版本和目标持仓生成可追溯指纹。发生差异时,可以先回答“输入不同、版本不同,还是决策不同”,避免把所有问题都归为市场噪声。
把时间隔离写成契约
历史回测必须显式保证第 日决策只使用第 日及更早数据;实时路径则在数据真正可得后才运行。两边的实现方式可以不同,但信息集合必须相同。
如果回测通过 shift(1) 隔离未来,模拟盘却在收盘前使用尚未完成的当日数据,它们仍然不是同一策略。
常见的伪修复
只让三份代码“看起来一样”
复制粘贴后逐行对齐只能解决当前差异,不能阻止下一次漂移。只要仍有三份实现,维护成本就会重新制造分叉。
只比较最终净值
净值相近可能只是误差互相抵消,净值不同也无法指出是哪一层造成。目标持仓、换手与订单路径比最终曲线更适合定位。
用更多测试保护重复实现
测试可以发现差异,却无法消除重复真相。优先收敛架构,再用测试守住唯一实现。
一份可执行的收敛清单
- 列出回测、模拟盘、实盘所有决策入口;
- 标记重复的清洗、因子、筛选、风控和调仓代码;
- 选定唯一的
SignalGenerator与RiskManager; - 让三条路径只提供标准化输入,并消费同一目标持仓输出;
- 建立固定输入的逐层对照测试;
- 在日志中记录数据截止时间、参数和策略版本;
- 删除旧实现,避免“暂时兼容”变成永久分叉。
从架构问题回到研究问题
只有当回测和执行共享同一条决策路径,回测指标才有资格参与实盘判断。否则,所谓“实盘验证回测”只是拿一套系统的结果解释另一套系统的行为。
One Codebase 的价值不是代码更整洁,而是让失败可以归因:输入、信号、风控、成交或市场变化,每一层都有可复核边界。
如果你的系统已经共享决策代码,但仍不确定因子公式本身在表达什么,可以回到价量因子怎么读,从输入、排名和方向三层建立阅读方法。