“回测收益高”是最容易骗人的指标。一个策略回测年化 50%,最大回撤 10%,Sharpe 2+,看起来非常漂亮。但如果把它拿到真实市场里跑,可能很快就发现完全不是那么回事。想分享一下我目前判断策略是否过拟合的一些方法。
1. 先把训练集和测试集分开
比如我有 2018 ~ 2025 年的历史数据,不能直接拿全部数据调参数,然后说这个策略在 2018 ~ 2025 年表现很好。因为策略参数本身就是根据这些数据调出来的。我一般会先分成:
- 2018 - 2022:训练集
- 2023 - 2024:验证集
- 2025:测试集
训练集用来开发策略和调参数。验证集用来检查策略是不是还能工作。测试集最好完全不碰,最后才拿出来做一次真正的 out-of-sample test 。 如果一个策略在训练集表现特别好,但到了验证集和测试集突然失效,我一般就会开始怀疑过拟合。
2. 参数稍微变化,策略就完全失效?
比如我做一个均线策略:
- fast_ma = 20
- slow_ma = 60
回测结果很好。 然后我把参数稍微改一下:
- fast_ma = 19, slow_ma = 60
- fast_ma = 21, slow_ma = 60
- fast_ma = 20, slow_ma = 59
- fast_ma = 20, slow_ma = 61
如果结果都差不多,我会觉得这个策略可能找到了某种比较稳定的市场规律。但如果:
- 20 / 60 → 年化 45%
- 19 / 60 → 年化 5%
- 21 / 60 → 亏损
- 20 / 59 → 亏损
- 20 / 61 → 亏损
这种情况我就会比较警惕。因为这很可能不是策略本身有效,而是参数刚好“撞上”了历史数据中的某个特殊模式。所以我现在倾向看参数附近的 performance surface 。一个真正有一定 robustness 的策略,通常不会只有一个孤立的最优点,而应该是一片相对稳定的区域。
3. 换一个市场还能不能跑?
比如我做的是一个美股策略。如果策略只在 SPY 上表现很好,我会尝试 SPY 、QQQ 、IWM 、DIA ,甚至换成其他市场。 当然,不是说一个策略必须在所有市场都赚钱。如果策略背后的逻辑是合理的,那么通常应该能够解释, 为什么它在这个市场有效。 如果只是“我在 SPY 上测试了 1000 组参数,最后发现这一组最好”,那我会觉得这个结果的可信度比较低。
4. 用 Monte Carlo 看看是不是太依赖某几笔交易
有些策略回测收益非常高,但仔细看发现,总收益的 70%来自 3 笔交易。这种策略我也会比较担心。我一般会尝试对交易顺序、收益序列做 Monte Carlo simulation 。例如原来的交易结果是:
- +5%
- -1%
- +2%
- -1%
- +20%
- -2%
- +3%
我可以随机打乱交易顺序,重复跑很多次。然后观察:
- Median CAGR
- 5% worst CAGR
- 95% best CAGR
- Max Drawdown distribution
- Probability of ruin
如果随机后的结果和原始结果差别非常大,说明策略可能比较依赖特定的交易顺序。这不一定意味着策略过拟合,但至少说明风险可能比回测结果看起来更高。
5. 不要只看收益率
除了看年化收益率、夏普比率、最大回撤,我还会看:
- 交易次数
- 胜率
- 盈利因子
- 单笔平均收益
- 平均持仓时间
- 换手率
- 年度收益表现
- 不同市场环境下的表现
- 不同波动率环境下的表现
尤其是交易次数。如果一个策略回测了 10 年,最后只有 20 笔交易,然后 CAGR 50%。我会觉得这个结果的统计意义可能不太够,因为样本太少了。反之,如果一个策略 10 年做了几万笔交易,虽然每笔平均收益只有 0.05%,但经过手续费和滑点之后还能赚钱,我反而会觉得这种结果可能更值得研究。
6. 实盘数据和回测数据一定要尽量一致
很多策略回测使用的是日 K ,但真正交易的时候却需要 Tick 级数据和订单簿。这时候很容易出现一个问题:回测里看起来你是在某个价格成交的,但真实市场里根本没有办法以那个价格成交。尤其是
- 高频策略
- 短周期策略
- 突破策略
- 新闻事件策略
- 盘口策略
这些策略对数据质量和时间精度都比较敏感。我自己在测试实时策略的时候,会用上实时行情,主要是想把策略回测和实时行情环境尽量连接起来:
import json
import websocket
API_KEY = "YOUR_API_KEY"
def on_message(ws, message):
data = json.loads(message)
print(data)
def on_open(ws):
print("WebSocket connected")
# 根据 API 文档发送订阅请求
subscribe_message = {
"action": "subscribe",
"symbols": ["AAPL"]
}
ws.send(json.dumps(subscribe_message))
ws = websocket.WebSocketApp(
f"wss://quote.alltick.co/quote-stock-b-ws-api?token={API_KEY}",
on_open=on_open,
on_message=on_message
)
ws.run_forever()
实战过程中要确保 [回测数据 → 策略信号 → 实时行情 → 模拟成交 → 实际交易] 这几个环节的数据定义尽量一致。否则可能出现一种情况:回测策略赚钱的原因,是因为你在历史数据里拥有“未来信息”。
7. Walk-forward Test
这是我目前比较喜欢的一种回测方法。 假设我有 2018 ~ 2025 年数据。 可以这样滚动测试:
Train: 2018-2020 Test : 2021
Train: 2019-2021 Test : 2022
Train: 2020-2022 Test : 2023
Train: 2021-2023 Test : 2024
Train: 2022-2024 Test : 2025
每次只使用过去的数据训练和优化参数,然后拿未来的一段数据测试。这样可以模拟真实交易过程中:我只能看到过去的数据,然后决定未来怎么交易。如果一个策略在多个 walk-forward window 里都表现不错,我会认为它的 robustness 比单次回测更高。
8. 我自己现在判断过拟合,大概会看这几个信号
如果一个策略出现下面这些情况,我会提高警惕:
-
参数稍微变化,收益就断崖式下降
-
训练集很好,测试集明显失效
-
换一个相似市场就完全不能用
-
收益高度依赖少数几笔交易
-
回测交易次数太少
-
加入手续费和滑点后直接亏损
-
使用更高频的数据后表现明显恶化
-
策略逻辑很难解释为什么有效
-
参数是经过大量试错后“挑”出来的
-
不同时间窗口测试结果差异非常大
尤其是第 9 个,我觉得很容易被忽略。如果我尝试了 500 个策略,最后只挑一个表现最好的出来,然后说:我的策略年化 100%。这个结果本身其实没有太大意义,因为我实际上是在做一种隐性的 multiple testing 。如果测试的策略足够多,总会有一些策略“碰巧”在历史数据里表现特别好。