Skip to content

改进方向-回测框架优化:回测偏差消除、生存偏差、前视偏差

做量化策略,最怕什么?

怕回测曲线漂亮得像假图,实盘一跑就原形毕露。

我见过太多团队,回测年化30%,实盘直接亏到怀疑人生。问题出在哪?十有八九是回测框架里有「脏东西」——各种偏差在作祟。今天咱们就聊聊怎么把这些坑填平。

回测偏差的三大元凶

说白了,回测偏差就是你的策略在历史数据上「作弊」了。不是故意的,但框架设计不严谨,就会偷偷给你加分。我总结下来,主要有三类:

  • 生存偏差:只看到活下来的股票,死掉的被忽略了
  • 前视偏差:用未来的信息做今天的决策
  • 其他常见偏差:幸存者偏差、过拟合偏差、交易成本偏差等

嗯,咱们一个一个拆解。

生存偏差:你看到的都是「活口」

生存偏差,也叫幸存者偏差。什么意思呢?

你回测时用的股票池,是今天还存在的股票。但十年前那些退市的、暴跌的、被ST的股票呢?它们的数据早就被剔除了。

这就好比只采访活着的百岁老人,然后得出结论「人人都能活到100岁」——荒谬吧?

核心问题:用当前存活的股票回测历史,会高估策略表现。因为那些死掉的股票,往往表现很差,它们本该拉低你的收益。

我在项目中遇到过一件事。有个团队开发了一个小市值策略,回测十年年化25%。结果实盘直接腰斩。为什么?因为回测时用的股票池是今天的,那些退市的小市值公司根本没算进去。而实际上,小市值公司退市率很高,一旦踩雷,损失惨重。

我的建议:回测时必须使用「历史成分股」数据。也就是每个时间点,只使用当时真实存在的股票。很多数据服务商提供「无生存偏差」的数据集,贵是贵点,但值得。

前视偏差:你偷偷看了「未来」

前视偏差,是回测框架里最常见的坑。说白了,就是你的策略在某个时间点,用到了未来才出现的信息。

举个例子:

# 错误示范:前视偏差
data['signal'] = (data['close'] > data['close'].shift(-1))  # 用了未来数据

这段代码里,shift(-1) 取的是明天的收盘价。你在今天就用明天的数据做判断——这不就是作弊吗?

我刚开始做量化时也犯过这种错。有一次回测一个突破策略,收益高得离谱。后来发现,我在计算突破信号时,不小心用了当天的最高价和最低价——而这两个价格在开盘时是不知道的。嗯,从那以后,我每次写回测代码都会反复检查数据对齐。

避坑指南:我曾经因为一个简单的 shift(-1) 没处理好,导致策略回测收益虚高15%。检查前视偏差的方法很简单:在回测代码里加一个「信息时间戳」字段,确保每个信号只使用该时间点之前的数据。

其他常见偏差

除了生存偏差和前视偏差,还有几个坑需要留意:

偏差类型 描述 改进方法
交易成本偏差 忽略滑点、手续费、冲击成本 按实际成本上浮20%-50%做压力测试
过拟合偏差 策略参数过度优化历史数据 使用交叉验证、样本外测试
流动性偏差 假设所有股票都能按理想价格成交 加入成交量限制、最小交易单位
时间偏差 使用未来数据做回测(前视偏差的一种) 严格按时间顺序回放数据

回测框架优化的核心思路

怎么优化?我总结了一个「三明治」框架:

  1. 数据层:使用无生存偏差的历史数据,确保数据完整
  2. 逻辑层:严格按时间顺序执行,禁止任何前视行为
  3. 验证层:加入交易成本、流动性限制,做压力测试

说白了,就是让回测尽可能接近真实交易环境。你想想看,如果回测时连手续费都不算,那结果能信吗?

知识体系结构图

下面这张图,是我自己画的一个回测偏差消除的流程框架。你可以把它当作检查清单:

flowchart TD
    A[数据层:消除生存偏差<br/>使用历史成分股数据 | 包含退市、ST股票 | 数据对齐时间戳]
    B[逻辑层:消除前视偏差<br/>严格时间顺序回放 | 禁止shift-1 | 信号生成只使用历史数据]
    C[验证层:消除其他偏差<br/>加入交易成本 | 流动性限制 | 过拟合检测 | 样本外测试]
    D[可信的回测结果]
    A --> B --> C --> D

实战中的改进技巧

光说不练假把式。我分享几个自己常用的改进方法:

技巧一:数据快照法

每次回测时,只使用该时间点之前的数据。比如你要回测2015年的策略,就只加载2015年之前的数据。别偷懒加载全量数据,否则很容易引入前视偏差。

技巧二:成本上浮法

我习惯在回测时把交易成本上浮30%-50%。比如实际手续费是万二,我按万三算。滑点也按保守估计。这样回测出来的结果,实盘时反而可能更好——因为实际成本往往低于你的假设。

技巧三:滚动回测法

别只做一次回测就完事。我建议做滚动回测:比如用3年数据训练,1年数据验证,然后窗口向前滚动。这样能有效检测策略是否过拟合。我曾经有个策略,全样本回测年化20%,滚动回测只有8%——果断放弃。

代码层面的检查清单

写回测代码时,我习惯在关键位置加断言检查:

# 检查前视偏差
def check_lookahead_bias(signal_df, price_df):
    """
    检查信号是否使用了未来数据
    """
    # 确保信号时间戳 <= 价格时间戳
    assert (signal_df.index <= price_df.index).all(), \
        "发现前视偏差!信号时间戳晚于价格时间戳"
    print("✅ 前视偏差检查通过")

# 检查生存偏差
def check_survivorship_bias(universe_df, current_stocks):
    """
    检查股票池是否包含已退市股票
    """
    dead_stocks = set(current_stocks) - set(universe_df.columns)
    if dead_stocks:
        print(f"⚠️ 发现 {len(dead_stocks)} 只已退市股票被排除")
    else:
        print("✅ 生存偏差检查通过")

嗯,这些检查虽然简单,但能帮你省下大量排查时间。我每次跑新策略前,都会先跑一遍检查脚本——就像开车前系安全带一样,成了习惯。

最后说两句

回测框架优化,说白了就是「别骗自己」。生存偏差让你高估收益,前视偏差让你误以为能预测未来,其他偏差则让策略在实盘时漏洞百出。

我个人的经验是:回测结果打个八折,才是实盘的合理预期。如果回测年化30%,实盘能到24%就算不错了。如果回测年化50%以上——嗯,大概率是框架有问题。

记住一句话:回测是用来发现问题的,不是用来证明策略牛逼的。