第二章 幸存者偏差:数据清洗中的生存偏差、指数重构偏差与规避方法

做套利策略回测,最怕什么?

怕数据骗你。

我见过太多新手,拿着漂亮的历史回测曲线兴奋得睡不着,结果实盘一跑就崩。为什么?十有八九是幸存者偏差在作祟。说白了,你看到的那些“历史表现优异”的股票,都是活到今天的。那些退市的、暴跌的、被收购的,早就被数据商剔除了。你想想看,这公平吗?

2.1 什么是幸存者偏差

幸存者偏差,就是只看到活下来的,忽略了死掉的。

在量化回测里,这个问题尤其隐蔽。你拿到的历史数据,默认只包含当前还在交易的股票。那些曾经辉煌过、后来退市的公司,它们的“黑历史”就被悄悄抹掉了。

核心问题:回测时你只做多“幸存者”,但实盘时你可能买入“未来退市者”。

举个例子。2007年,中国石油上市,股价48块。后来一路跌到4块。如果你回测时只保留今天还在的股票,中石油这种“巨亏案例”就被剔除了。你的回测曲线自然漂亮得不像话——但那是假的。

我个人习惯,拿到数据第一件事,先查退市名单。看看数据里有没有包含那些“失败者”。没有?那这数据就不能直接用。

2.2 生存偏差的两种常见形式

生存偏差不是只有一种。我把它分成两类,你在实际项目中都会遇到。

2.2.1 退市偏差

这是最直接的。公司退市了,数据就没了。但退市前的那段暴跌,恰恰是回测里最该体现的风险。

典型场景:

  • 财务造假被强制退市(如瑞幸咖啡)
  • 连续亏损导致股价归零
  • 私有化退市(如一些中概股)

我在项目中遇到过,回测一个低估值策略,年化收益20%+,夏普比2.5。结果一查,数据里少了30多只退市的银行股。这些银行股当年估值也低,但后来坏账爆发,股价跌了90%。加上它们,策略收益直接腰斩。

2.2.2 指数重构偏差

这个更隐蔽。指数会定期调整成分股——涨得好的留下,跌得惨的踢出去。你回测时用的是调整后的成分股,但实盘时你买的是调整前的。

为什么会这样?

因为指数公司只保留“好学生”。比如沪深300,每年6月和12月调仓。那些跌成狗的股票被踢出指数,数据商也就不再提供它们的历史数据了。你回测时看到的沪深300,其实是“被美化过的沪深300”。

注意:指数重构偏差会让你的回测收益虚高2%-5%每年。别小看这个数字,复利下来差距巨大。

2.3 如何检测幸存者偏差

检测方法其实不复杂。我分享三个实用技巧。

2.3.1 检查退市标记

正规数据源(如Wind、Tushare)会提供退市标记字段。你只需要统计一下,数据里有多少股票是“已退市”状态。

# 检查退市比例
import pandas as pd

# 假设df包含股票列表,'status'字段标记是否退市
df = pd.read_csv('stock_list.csv')
delisted = df[df['status'] == '退市']
print(f"退市股票数量: {len(delisted)}")
print(f"退市比例: {len(delisted)/len(df):.2%}")

# 如果退市比例接近0%,说明数据有问题
if len(delisted) < 10:
    print("⚠️ 警告:退市股票过少,可能存在幸存者偏差")

2.3.2 对比指数历史表现

把你的回测收益曲线和真实指数对比。如果回测收益明显高于指数,而且差距稳定,那大概率是幸存者偏差。

我记得有一次,一个同事拿来的回测曲线年化25%,同期沪深300只有8%。他兴奋得不行。我让他把退市股票加回去,结果收益直接掉到10%。嗯,真相大白。

2.3.3 随机抽样验证

从历史数据中随机抽取一些股票,手动查一下它们现在的状态。如果抽到的股票大部分都还活着,那数据可能有问题。

小技巧:我一般会抽50只股票,用百度或财经网站查它们是否退市。如果退市比例低于5%,我会怀疑数据源。

2.4 规避方法:数据清洗实战

知道了问题,怎么解决?我给出三个层面的方案。

2.4.1 使用全量历史数据

不要只拿当前成分股。要拿“历史上曾经存在过的所有股票”。

具体做法:

  • 从数据商获取“全量股票列表”,包含已退市股票
  • 回测时,只使用当时已经上市的股票
  • 退市股票的价格数据要保留到退市当天
# 正确的回测数据准备
def prepare_backtest_data(all_stocks, trade_date):
    """
    获取指定交易日可交易的股票列表
    """
    # 只保留当天已上市且未退市的股票
    available = all_stocks[
        (all_stocks['list_date'] <= trade_date) &
        ((all_stocks['delist_date'].isna()) | 
         (all_stocks['delist_date'] >= trade_date))
    ]
    return available['stock_code'].tolist()

2.4.2 指数重构的模拟

如果你回测的是指数增强策略,必须模拟指数的调仓规则。

我建议的做法:

  1. 获取指数编制方案(比如沪深300的选股规则)
  2. 在回测中,按规则定期重新计算成分股
  3. 不要直接使用指数公司提供的“当前成分股历史数据”

关键点:指数重构偏差的规避,核心是“用当时的规则选当时的股票”。不要事后诸葛亮。

2.4.3 数据源交叉验证

不要只依赖一个数据源。我习惯用两个独立数据源做交叉验证。

数据源 优点 缺点
Wind 退市数据完整,更新及时 价格较高
Tushare 免费,社区活跃 退市数据可能不完整
RiceQuant 自带回测框架,数据清洗较好 定制化程度有限

我曾经用Tushare跑了一个策略,收益不错。后来用Wind一对比,发现Tushare少了200多只退市股票。嗯,从那以后我再也不只用单一数据源了。

2.5 幸存者偏差的SVG框架图

下面这张图,帮你理清幸存者偏差的来龙去脉。

幸存者偏差:从数据到回测的陷阱 原始全量数据 数据清洗(剔除退市股) 幸存者数据 回测收益虚高 退市偏差 指数重构偏差 规避方法 方法1:使用全量历史数据 包含退市股票 方法2:模拟指数调仓规则 避免事后诸葛亮 方法3:多数据源交叉验证 Wind + Tushare 图:幸存者偏差的形成路径与规避方法

2.6 避坑指南:我的血泪教训

最后,分享几个我踩过的坑。希望能帮你省点时间。

避坑1:我曾经用某免费数据源跑了一个多因子策略,回测夏普比3.0。结果发现数据里只有当前存活的股票,那些因子失效的股票全被剔除了。加上退市股后,夏普比掉到1.2。嗯,从那以后,我再也不信免费数据了。

避坑2:做指数增强策略时,我直接用了指数公司提供的“历史成分股数据”。结果发现,这些数据是“当前成分股的历史数据”,而不是“历史成分股的真实数据”。两者差别巨大。后来我改用指数编制方案自己算成分股,才解决了这个问题。

我的习惯:每次回测前,我都会做一次“退市股票敏感性分析”。把退市股票加回去,看看收益变化。如果变化超过20%,那这个策略基本不能实盘。

幸存者偏差,说白了就是数据在骗你。你看到的“历史”,是被筛选过的历史。只有把那些“失败者”请回来,你才能看到真实的回测结果。

记住一句话:回测里没有幸存者偏差,实盘里全是幸存者偏差。


公众号:蓝海数据掘金营,微信deep3321