回测很好,模拟盘却持续跑输,第一反应常常是“因子失效了”。这次事故的根因更朴素:回测每 5 个交易日调一次仓,模拟盘却每天按最新排名重建持仓。两边看似使用同一批因子,实际运行的是两套不同策略。

本文只复盘已公开的工程事实,不展示未公开的账户收益,也不把单次事故泛化成所有策略的规律。

为什么一天和五天不是小参数差异

假设因子每天都会给出一组目标持仓。相邻两天的排序即使只轻微变化,Top N 边界附近的股票也可能频繁进出组合。

回测每 5 天才执行一次,意味着中间 4 天的排名抖动不会变成订单;模拟盘每天重建,则会把这些抖动全部兑现为买卖。最终成本大致由三部分共同决定:

costtturnovert×(commission+spread+slippage)\mathrm{cost}_t \approx \mathrm{turnover}_t \times (\mathrm{commission} + \mathrm{spread} + \mathrm{slippage})

这里最重要的不是某个固定费率,而是换手率被执行频率放大。即使毛收益信号没有变化,净收益也可能被额外摩擦吞掉。

事故是怎样被定位的

先停止讨论“市场变了”

如果回测和模拟盘不是同一个策略,比较它们的收益没有意义。定位时先并排检查四个事实:

检查项回测模拟盘是否一致
信号生成日每个交易日每个交易日
允许下单日每 5 个交易日每个交易日
非调仓日行为保持持仓重建目标持仓
成本来源按回测换手估算按真实订单产生结果不可直接比较

这张表一旦出现“不一致”,就应该先修复执行契约,再讨论 Alpha 衰减。

再看订单,而不是只看净值

净值只能告诉你出了问题,订单日志才能解释问题。至少要逐日保留:

  • 当日是否为允许调仓日;
  • 调仓前持仓与目标持仓;
  • 买卖金额、组合换手率与成交数量;
  • 预期价格、实际成交价与滑点;
  • 非调仓日是否产生了不该出现的订单。

当非调仓日仍出现大量目标持仓变更时,频率错配就从猜测变成了可复核证据。

最小复现实验怎么做

不要先重跑整套参数搜索。固定同一份输入数据、同一份信号和同一组成本假设,只改变执行日历:

  1. A 组每个交易日允许调仓;
  2. B 组每 5 个交易日允许调仓;
  3. 同时比较毛收益、净收益、平均换手、订单数和持仓重合度;
  4. 检查两组在允许调仓日之外是否保持相同持仓;
  5. 不用单个夏普比率替代上述过程指标。

这个实验回答的是“频率改变了什么”,而不是寻找表现最好的频率。先把事故因果关系隔离出来,再决定策略应该用什么节奏。

修复不只是加一个 if

事故中的直接修复是在执行入口增加 should_rebalance:只有命中与回测一致的调仓日历才生成订单,其他日期保持持仓。

但只加条件仍不够。为了避免下一次漂移,调仓频率应该成为回测与执行共同读取的策略契约,并配套以下保护:

  • 单元测试:非调仓日订单数必须为零;
  • 集成测试:同一输入下,回测与模拟盘输出的目标持仓一致;
  • 监控:实际换手显著高于回测口径时告警;
  • 日志:每次下单记录触发原因与策略版本。

只要回测和执行各自维护一份“默认值”,同类问题就可能再次出现。

排查顺序比结论更重要

遇到回测—实盘落差,可以按下面的顺序收缩问题:

  1. 信号使用的数据时点是否一致;
  2. 股票池、筛选、Top N 与权重是否一致;
  3. 调仓日历与非调仓日行为是否一致;
  4. 风控、整股取整和现金约束是否一致;
  5. 手续费、点差、滑点和成交失败是否进入结果;
  6. 上述全部对齐后,再讨论市场环境和因子衰减。

这次事故给出的不是“周频一定优于日频”。真正的教训是:未经回测验证的执行频率,就是一套未经验证的新策略。

下一篇将继续处理更深一层的问题:即使频率参数相同,回测、模拟盘和实盘各自复制了一份信号与风控代码,结果仍然会逐渐分叉。