回测可信度审查:样本外测试的五种常见自欺
本文是《Python 量化回测系统工程》系列的第 2 篇。上一篇讲了滑点和手续费必须默认开启,这一篇讲一个更隐蔽的问题:你辛辛苦苦做的"样本外测试",可能根本不算样本外。
一、先说一个反直觉的事实
很多人的验证流程是这样的:拿一段历史数据调参数,调到曲线好看,然后换一段没用过的数据跑一遍------跑赢了,就宣布"通过样本外验证"。
问题是:这一步几乎证明不了任何事。
原因很简单。你在调参时看过全样本的分布特征、看过最大回撤出现在哪、看过哪段行情最难熬。这些信息已经进了你的脑子,也进了你最终选定的那组参数。之后再切一段数据出来跑,它和"样本外"这三个字要求的独立性,差得很远。
真正可信的样本外,成本比大多数人愿意付的高得多。下面五种是最常见的自欺方式,按隐蔽程度排序。
二、五种自欺
1. 参数是在全样本上挑的,样本外只是"复述"
症状:先跑全区间,看哪组参数最好,然后宣布"这组参数在样本外也表现不错"。
为什么是自欺 :参数的挑选过程已经用到了样本外区间的信息。这叫窥探偏差(look-ahead in selection),它不像未来函数那样显眼,所以更难被发现。
工程上的解法 :参数选择必须在训练段内完成,且训练段要能真正封死------写进配置、落盘、带时间戳。选完之后,样本外区间只能跑一次,结果直接进报告,不允许回头改参数。
2. 跑输了就换一组参数再跑一次
症状:样本外跑输了,"这组参数可能过拟合了",换一组再试,直到跑赢为止。
为什么是自欺 :这是多重检验问题。你不是在验证一个假设,你是在用随机性刷显著性。跑 20 次,哪怕策略毫无预测能力,也总有一两次看起来漂亮。
工程上的解法 :样本外只允许跑一次 。次数写进报告头部。如果确实需要迭代,就换一套全新的、从未参与过任何调参的数据,并且把前面所有次的尝试都记进台账------不是藏起来,是摊开。
3. 样本外区间太短,样本量根本不够
症状:样本外只有两三个月、几十笔交易,然后拿"年化收益"当结论。
为什么是自欺:几十笔交易的波动区间宽到能吞掉任何结论。你算出来的"年化 15%",置信区间可能是 -20% 到 +50%------它不叫结论,叫噪声。
工程上的解法 :给出样本量下限,达不到就明确标注"样本不足,结论仅供参考"。同时分段统计:把样本外再切成若干子段,看结论在多少个子段上成立。全区间一个数字很漂亮、子段上一半在亏,这本身就是过拟合的信号。
4. 切法让两段数据"长得一样"
症状:随机打散数据切训练/测试集。
为什么是自欺 :时间序列不能随机切。随机切会把相邻 K 线分到两边,两段数据的市场状态高度雷同,测试集等于训练集的重影。这是把时序问题当成截面问题处理,也是从机器学习照搬方法论时最容易犯的错。
工程上的解法 :严格按时间切,且留出缓冲带(训练段末尾和测试段开头之间空出一段不用),避免相邻bar的信息渗透。更严的做法是 walk-forward:滚动推进,每个窗口重新训练、只测下一段。
5. 只看收益,不看"结论有多脆"
症状:样本外跑赢了就收工,从不问"参数稍微动一下会怎样"。
为什么是自欺:如果最优参数周围一圈全是坑------参数从 20 挪到 21 收益直接腰斩------那你找到的不是规律,是噪声里的一个尖峰。实盘里任何微小的市场变化都会把你从尖峰上推下去。
工程上的解法 :跑参数邻域敏感性网格。把主参数在其最优值附近做网格展开,看收益曲面是平缓的高地还是孤立的针尖。高地才能用,针尖必须弃。
三、把这五条变成一份可执行的验证协议
我们把它固化成了三段式,每次交付前必跑:
| 阶段 | 数据 | 允许的操作 | 不允许的操作 |
|---|---|---|---|
| 样本内 | 训练段 | 调参、选结构、剪枝 | --- |
| 样本外 | 紧邻的下一段,含缓冲带 | 只跑一次,结果直接进报告 | 回看后改参数、换参数重跑 |
| 跨品种 | 其它品种同期数据 | 跑一次,看结论是否迁移 | 针对该品种重新调参 |
配套三条硬要求:
- 参数邻域网格:主参数至少做 3×3 展开,收益曲面必须是高地不是针尖;
- 分段统计:样本外至少再切 3 个子段,结论在多数子段上成立才算通过;
- 滑点敏感性:和上一篇一样,0 / 1 / 2 跳三档都要跑,三档结论方向必须一致。
四、用一组真实数字看什么叫"结论很脆"
下面是同一套引擎、同一份作业,唯一区别是滑点档位的三次运行结果(资金 100 万,日线,含手续费):
| 滑点档位 | 累计收益 | 最大回撤 |
|---|---|---|
| 0 跳 | +6.24% | 0.49% |
| 1 跳 | +2.72% | 1.08% |
| 2 跳 | +2.49% | 1.16% |
收益从 +6.24% 掉到 +2.49%,掉了六成;最大回撤反而从 0.49% 扩大到 1.16%。同一个"策略",在不同摩擦假设下是两回事。
另外几个同批次的数字:期末权益 1,024,907.77,胜率 60%,盈亏比 3.09,滑点审计出来的平均滑点是 +3.00 跳。
注意最后那个数------审计出来的实际滑点(3.00 跳)比我们测试时假设的最严档位(2 跳)还要大 。这就是为什么报告里必须同时给两个数:回测数,和一个打了折的保守参考数。我们的经验系数是 ×0.55(样本外 0.7 × 实盘 0.8),两个数分两栏放,读者自己判断。
五、三条给工程师的建议
- 验证的成本要花在"让数据变干净"上,不是"让曲线变好看"上。一次诚实的失败验证,比十次漂亮的可疑验证值钱;
- 把"我跑过几次"写进报告。次数是关键元数据,藏起来就是自欺的开始;
- 结论的脆性比结论本身重要。能说清"这个结论在什么条件下会失效",才算真懂了这个结论。
下一篇预告
《先算这一笔:用一个公式判断你的收益目标做不做得到》------讲「所需 Calmar = 年化目标 ÷ 回撤容忍度」,以及为什么在动手写第一行回测代码之前,就该先把这个数算出来。
本文数据来自一套真实的 Python 回测作业(演示模式),所有数字可复现。系列文章只讲工程与数据口径,不涉及任何具体策略、指标与参数。