3. 回测陷阱与生存偏差:前视偏差、生存偏差、过拟合回测的识别与修正

做量化策略,最怕什么?

不是亏钱,而是你明明觉得策略很稳,一上实盘就崩。我见过太多人,回测曲线漂亮得像教科书,实盘却像过山车。说白了,问题往往出在回测本身——你踩了陷阱,自己还不知道。

这一章,咱们就聊聊三个最常见的回测陷阱:前视偏差、生存偏差、过拟合。我会结合自己的踩坑经历,告诉你如何识别,怎么修正。

3.1 前视偏差:你看到了未来

前视偏差,也叫未来函数。简单说,就是你用未来的信息去决策今天的交易。这在回测里特别隐蔽。

我遇到过的一个典型例子:

有个团队做因子回测,用当天的收盘价计算信号,然后当天买入。你想想看,收盘价出来的时候,交易时间已经结束了。这等于你提前知道了收盘价,再去下单——这不就是作弊吗?

核心判断标准:

回测中,任何用到未来数据的操作,都是前视偏差。

常见的几种前视偏差:

  • 价格类未来函数:用当日最高价、最低价、收盘价计算信号,然后当日交易。
  • 财务数据未来函数:用季报发布后的数据,但回测中假设数据在季报截止日就能用。实际上,财报发布有延迟。
  • 事件类未来函数:比如用“涨停”作为信号,但涨停后你根本买不进去。

怎么修正?

我个人习惯,在回测框架里强制加入一个“数据延迟”机制。比如,用当天的数据,只能生成明天的信号。代码里可以这样写:

# 错误示例:前视偏差
df['signal'] = (df['close'] > df['close'].shift(1))  # 用当天收盘价
df['position'] = df['signal'].shift(0)  # 当天开仓

# 正确示例:避免前视偏差
df['signal'] = (df['close'].shift(1) > df['close'].shift(2))  # 用昨天数据
df['position'] = df['signal'].shift(1)  # 今天开仓

小技巧:写回测时,养成一个习惯——所有用到价格的地方,都先 .shift(1)。这样能挡住90%的前视偏差。

3.2 生存偏差:活下来的都是英雄

生存偏差,指的是你的回测数据里,只包含了现在还活着的股票。那些退市、被ST、暴跌的股票,早就被剔除了。

为什么这是个陷阱?

你想想看,如果只拿活着的股票做回测,策略表现当然好。因为那些烂股票已经被历史淘汰了。但实盘时,你可能会买到一只即将退市的股票——回测里根本没它的数据。

我曾经踩过的坑:

几年前,我做一个小市值策略。回测年化收益30%,最大回撤不到10%。我兴奋得不行。结果实盘跑了三个月,亏了15%。后来一查,回测数据里少了大量退市的小盘股。那些股票在回测期间暴跌,但数据里没有它们——这就是生存偏差。

怎么识别?

  • 检查数据源:是否包含退市、ST、暂停上市的股票?
  • 对比全样本和存活样本的收益分布。如果存活样本收益明显更高,那就有问题。

修正方法:

我建议,回测时一定要用“全样本数据”,包括已经退市的。如果数据源不完整,至少要在回测中模拟退市风险。比如,随机剔除一部分股票,看看策略是否还能赚钱。

警告:很多免费数据源默认只提供当前存活的股票。如果你不做处理,生存偏差会严重高估策略表现。

3.3 过拟合回测:曲线漂亮,实盘拉胯

过拟合,是量化策略的头号杀手。你不断调整参数,直到回测曲线完美。但实盘时,市场一变,策略就失效。

过拟合的典型特征:

  • 回测收益极高,夏普比率超过3甚至5。
  • 参数稍微一改,收益就大幅下降。
  • 策略在样本外测试中表现很差。

我见过最夸张的例子:

有个朋友,把50个参数优化到极致,回测年化收益80%。我问他:“你参数怎么选的?”他说:“一个一个试,找到最好的组合。” 嗯,这其实就是过拟合。后来实盘,三个月亏了40%。

怎么识别过拟合?

我个人习惯用“夏普比率衰减测试”。具体做法:

  1. 把数据分成训练集和测试集(比如前70%训练,后30%测试)。
  2. 在训练集上优化参数。
  3. 在测试集上验证。如果测试集夏普比率比训练集低很多(比如低50%以上),那大概率过拟合了。

修正方法:

  • 减少参数数量:参数越少,过拟合风险越低。我一般控制在3-5个以内。
  • 交叉验证:用滚动窗口的方式,反复训练和测试。
  • 正则化:在优化目标中加入惩罚项,防止参数过大。
# 简单的交叉验证示例
from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)
for train_index, test_index in tscv.split(data):
    train_data = data.iloc[train_index]
    test_data = data.iloc[test_index]
    # 在train_data上优化,在test_data上验证

核心原则:回测曲线越漂亮,你越要怀疑。真正稳健的策略,回测曲线往往没那么惊艳。

3.4 知识体系总览

为了让你更直观地理解这三个陷阱的关系,我画了一张图。它展示了回测陷阱的识别与修正流程。

回测陷阱识别与修正流程 回测策略 前视偏差 生存偏差 过拟合 数据延迟 + shift 全样本数据 交叉验证 + 少参数 稳健的回测结果

这张图的核心逻辑很简单:回测策略出来后,先检查有没有前视偏差、生存偏差、过拟合。如果有,就用对应的修正方法处理。最后,才能得到一个相对稳健的结果。

3.5 避坑指南

最后,分享几个我自己的避坑经验:

  • 我曾经因为数据源没更新退市股票,白高兴了一个月。现在,我每次回测前都会检查数据是否包含退市样本。
  • 我曾经为了追求高夏普,把参数优化到极致。结果实盘一塌糊涂。现在,我宁愿要一个夏普1.5但稳健的策略,也不要夏普3但脆弱的策略。
  • 我建议,每次回测后,都做一次“压力测试”:随机改变一些参数,看看策略是否还能赚钱。如果稍微一变就亏,那说明过拟合了。

一句话总结:回测不是用来证明策略有多好,而是用来发现策略有多烂。你越早发现陷阱,实盘就越安全。

专注资料整理