为什么漂亮的回测可能什么都不代表——以及如何分辨真正的优势和曲线拟合的假象。
你扫完参数,挑了 Sharpe 最高的那一组,回测曲线漂亮得不像话。真的跑起来之后,它的表现跟随机没有差别。中间发生的事有个名字。
当一个策略的参数被精确调整到能够完美符合历史数据时,它实际上是在记忆过去,而不是学习可以泛化的规则。回测看起来出色,实盘却表现很差——甚至和随机没有差别。
过拟合通常不是有意为之的。每当你做了以下事情,它就自然发生:
假设你扫描了 100 个参数组合。就算你的策略完全没有优势,在 5% 的显著性水平下,单纯靠概率就有大约 5 个组合会产生正的 Sharpe。如果你挑出最好的那个并汇报为策略绩效,你是在把噪声当信号。
这个问题随着参数数量增加而恶化。两个参数做 20×20 的网格有 400 个组合;四个参数做 10×10×10×10 有 10,000 个。每增加一个维度,撞上幸运峰值的概率就翻倍。
避免过拟合最重要的实务技巧是:不要挑峰值。
看一张参数扫描的 Sharpe 热力图。绝对最佳的格子可能显示 Sharpe 1.8。往左移一格:0.4。往上移一格:0.3。这个峰值很脆弱——它之所以存在,是因为那个特定的参数组合刚好符合了训练数据里几个特定的市场事件。
正确的做法是找高原(Plateau):在许多邻近参数组合上 Sharpe 都持续偏高的区域。如果一个 3×3 邻域的 Sharpe 平均都在 1.2 左右,这才是稳健的信号——优势存在于一个参数范围之内,不只在单一的精确值。
# Blave Agent 的 find_plateau: # 选的是邻域(3×3 窗口)平均 Sharpe 最高的格子, # 而不是单点最大值。 best_idx = argmax(neighborhood_average_sharpe)
这就是为什么参数扫描脚本——每支策略自己文件夹里的 scan.py——用 find_plateau 而不是直接取最佳 Sharpe。位于平滑高原上的参数组合,远比位于孤立峰值的组合更可能泛化到未来。
衡量策略真实绩效的唯一有效方式,是在它从未「见过」的数据上测试。如果你用 2020–2023 的数据优化参数,然后在 2024 的数据上评估,2024 的绩效就是真正的样本外估计。
如果你用 2020–2024 的数据优化,再汇报 2020–2024 的绩效:那是样本内。即使你并没有刻意过拟合,优化过程本身已经隐性地使用了 2020–2024 的结果来选择参数。汇报的 Sharpe 包含了后见之明的好处。
| 方法 | 有效? | 说明 |
|---|---|---|
| 用全部历史数据优化,汇报全部历史绩效 | ✗ | 样本内——必然过拟合 |
| 用前 70% 优化,汇报后 30% 绩效 | ✓ | 简单训练/测试分割——有效但有限 |
| 滚动前推:用滚动窗口优化,汇报下一段 | ✓✓ | 最真实——和实盘的方式相同 |
另一个常见的过拟合陷阱是把扫描范围设得太广,包含在历史上几乎从未出现过的极端参数值,让某个异常市场期的偶然事件拉高那个参数的 Sharpe。
Blave Agent 对阈值型策略采用不同的做法:从指标本身的分布来决定扫描范围。先取指标历史分布的 p5 与 p95 当两端,再用两者的算术中点(不是中位数)当分界;入场阈值的候选值落在上半段(中点到 p95),出场阈值的候选值落在下半段(p5 到中点)。这让两个阈值都落在有足够历史数据的范围内,不会把历史上只出现过两次的极端值当作候选。
Sharpe 热力图上的孤立峰值。 最佳参数组合的邻近格子 Sharpe 大幅下跌。往任何方向移动一格就掉 0.5 以上。优势不稳健,只是局部的。
回测 Sharpe 在样本外大幅下滑。 训练期 Sharpe 2.0,测试期 Sharpe 0.2。落差越大,策略越过拟合。
交易笔数太少。 三年回测只有 12 笔交易,几乎没有统计效力。几笔幸运的交易就能纯靠概率制造 Sharpe 2.0。需要至少 30–50 笔完整交易,回测数字才有意义。
绩效完全集中在单一市场环境。 如果回测绩效完全来自某一段牛市或熊市,其他时期都是横盘或亏损,策略可能只是学会了那个特定的市场环境,而不是一个普遍的规则。
为了「修掉」亏损期而加入参数。 在图表上看到某段回撤后,特意加入一个条件来避开它,然后在同样的数据上重测——这是最典型的过拟合操作,因为未来不会再有一模一样的亏损期等你去避开。
没有通用的上限,但有一个实用原则:每一个额外的自由参数都应该有先验的理由存在于策略逻辑中。不是因为加了它能让数据拟合得更好,而是因为它代表了你所利用的市场机制的一个真实维度。
一个有两个参数、代表有意义概念的策略(例如快速信号窗口和慢速确认窗口),比一个有六个参数、其中几个是为了「修掉」图表上特定亏损期而加入的策略,更有说服力。