🏠
AI量化交易 · 第二十二章
目录
CHAPTER 22

回测的局限性

为什么99%的策略回测结果都无法在实盘复制

本章导航

  1. 学习目标
  2. 故事引入:一个完美的回测,一场真实的噩梦
  3. 核心知识:回测的六大陷阱
  4. AI实践:让AI帮你识别回测陷阱
  5. 动手练习:回测质量检查清单代码
  6. 本章测验
  7. 下一章预告

学习目标

回测是量化策略的试衣间——你可以在投入真金白银之前,看看策略在历史数据上的表现。但试衣间的镜子可能变形,衣服可能加了内衬,灯光可能经过特殊设计。回测结果和实盘结果之间的差距,是量化交易中最昂贵的一堂课。

本章结束后,你将能够:

故事引入:一个完美的回测,一场真实的噩梦

2019年,程序员小张花了三个月开发了一个AI选股策略。回测结果令人窒息地美丽:2015-2018年,年化收益127%,最大回撤仅8%,夏普比率3.5。曲线几乎是一条45度向上的直线。

小张辞掉了工作,凑了50万本金,信心满满地实盘运行。前三个月还算平稳,第四个月开始连续亏损,第六个月净值跌破0.7。他止损出局时,50万只剩31万。

问题出在哪里?事后复盘,小张的策略至少犯了四个致命错误:

  1. 未来函数:策略使用了当日收盘价作为买入信号,但交易执行也是在当日收盘价——这在现实中不可能,因为你收盘前不知道收盘价是多少。
  2. 幸存者偏差:回测使用的股票池只包含当前还活着的股票,所有退市、ST、停牌的股票都被悄悄删除了。
  3. 成本忽略:回测假设零手续费、零滑点。策略每天交易20只股票,实际交易成本eat掉了大部分利润。
  4. 过拟合:策略有17个参数,通过网格搜索在历史数据上优化到了极致——它完美拟合了历史噪声,但对未来一无所知。
回测最大的危险,不是它告诉你策略会亏钱——而是它让你相信策略会赚钱。一个糟糕的策略配上一个有偏的回测,是散户走向破产的最快路径。

核心知识:回测的六大陷阱

陷阱一:未来函数(Lookahead Bias)

未来函数是最隐蔽、最常见、也最具毁灭性的回测错误。它的本质是:你在做决策时使用了当时还无法获得的信息。

典型的未来函数包括:

# ❌ 错误示例:未来函数
signal = df['close'] > df['ma20']
execute_price = df['close']
# 问题:收盘前你不知道收盘价是否高于MA20

# ✅ 正确做法:使用滞后数据
signal = df['close'].shift(1) > df['ma20'].shift(1)
execute_price = df['open']

# ❌ 错误示例:财报日期错误
df['use_earnings'] = df['quarter_end']

# ✅ 正确做法:使用实际公告日期
df['use_earnings'] = df['announcement_date']

陷阱二:幸存者偏差(Survivorship Bias)

假设你在2024年下载了一份A股历史数据,然后开始回测。你有没有想过,这份数据里包含的所有股票,都是到今天还活着的?那些退市、破产、被借壳的公司呢?它们被悄无声息地删除了。

如果你在2008年用某个价值因子选股,你很可能会选中后来ST、退市的股票。但在2024年的数据集中,这些公司不存在了,于是你的回测结果异常美丽。

如何修复:必须使用包含已退市股票的历史数据。对于A股,可以通过专业数据商获取退市股票列表,并在回测时把它们加回来。

陷阱三:成本低估

新手回测最常见的假设是零成本交易。现实中,成本是一座大山:

def apply_transaction_costs(returns, turnover, fee_rate=0.00025,
                            stamp_tax=0.0005, slippage=0.001):
    """
    对收益率扣除交易成本
    turnover: 日换手率(买卖双边)
    fee_rate: 佣金率(默认万2.5)
    stamp_tax: 印花税(卖出时,默认万5)
    slippage: 滑点(双边,默认千1)
    """
    buy_cost = turnover * fee_rate + turnover * slippage
    sell_cost = turnover * (fee_rate + stamp_tax + slippage)
    total_cost = buy_cost + sell_cost
    net_returns = returns - total_cost
    return net_returns

# 示例:一个日换手率50%的策略,年化成本有多恐怖?
daily_turnover = 0.5
annual_turnover = daily_turnover * 252
cost_per_year = annual_turnover * (0.00025*2 + 0.0005 + 0.001*2)
print(f"年化交易成本: {cost_per_year:.2%}")
# 结果可能高达30-40%,足以吞噬大部分策略收益

陷阱四:过拟合(Overfitting)

过拟合是量化策略的癌症。它发生在策略参数过多、优化过于精细的情况下——策略完美拟合了历史数据的每一个波动,包括噪声。

识别过拟合的信号:

  1. 参数过多:策略有10个以上可调参数,而你只回测了5年数据。参数越多,过拟合风险越大。
  2. 收益曲线过于平滑:真实策略的收益曲线应该是有波动的。如果回测曲线像一条直线,很可能你在某处偷偷作弊了。
  3. 样本外表现骤降:策略在训练期(2015-2020)表现极好,在测试期(2021-2024)表现极差。
  4. 参数高原过于狭窄:最优参数点附近稍微变动,收益就大幅下降。这说明策略依赖的是历史巧合,而非稳健规律。
⚠️ 点击展开:过拟合的数学直觉

假设你有N个独立交易信号,每个信号有50%的概率在未来有效。如果你测试了100个信号, purely by chance,大约有5个信号会在5%显著性水平下看起来有效(这就是多重比较问题)。

更直观的例子:你扔100次硬币,有人告诉你如果某次连续出现7次正面,就说明这枚硬币有问题。你扔着扔着,某一段确实出现了7次正面——但这完全是随机的。如果你在发现7次正面之后才开始下注赌正面,期望收益仍然是零。

回测中的过拟合,本质上就是在历史数据中寻找碰巧出现的7次正面,并误以为这是规律。

陷阱五:过度交易与容量幻觉

很多策略回测时假设可以无限量地以收盘价成交。现实中:

陷阱六:隐含假设与数据质量

数据本身也可能撒谎:

与AI一起练习

练习1:让AI审查你的回测代码

请审查以下回测代码,找出所有潜在的未来函数、幸存者偏差、成本低估问题:
[粘贴你的回测代码]
要求:逐行标注问题,给出修改建议,并估算这些问题对回测收益的高估幅度。

练习2:让AI设计一个回测质量检查清单

请为我设计一个量化策略回测质量检查清单(Checklist),包含:
1. 数据质量检查(10项)
2. 未来函数排查(8项)
3. 成本与流动性检查(6项)
4. 过拟合风险评估(5项)
每项给出检查方法和通过标准。

练习3:让AI对比回测与实盘差异

我有一个策略回测年化收益40%,夏普2.0,最大回撤10%。
请帮我分析:如果这个策略实盘运行,哪些因素会导致收益下降?
请给出各项因素的量化估算(如滑点影响、容量限制、情绪偏差等),
并预测实盘可能的收益区间。

动手练习:回测质量检查清单代码

"""回测质量检查器 - 在相信回测结果之前,先运行这个"""
import pandas as pd
import numpy as np

class BacktestAuditor:
    def __init__(self, price_df, signal_df, trades_df):
        self.price = price_df
        self.signal = signal_df
        self.trades = trades_df
    
    def check_lookahead_bias(self):
        """检查未来函数"""
        issues = []
        # 检查信号是否使用了未来数据
        if 'close' in self.signal.columns:
            if (self.signal.index == self.price.index).all():
                issues.append("警告:信号与价格同日期,可能存在未来函数")
        return issues
    
    def check_survivorship_bias(self):
        """检查幸存者偏差"""
        total_stocks = self.price['code'].nunique()
        # 如果股票池在过去几年一成不变,可能存在幸存者偏差
        yearly_counts = self.price.groupby(
            self.price.index.year)['code'].nunique()
        if yearly_counts.std() < total_stocks * 0.05:
            return ["警告:股票池数量年度变化极小,可能存在幸存者偏差"]
        return []
    
    def estimate_costs(self, turnover_estimate=0.3):
        """估算交易成本对收益的影响"""
        fee = 0.00025
        stamp = 0.0005
        slip = 0.001
        daily_cost = turnover_estimate * (fee*2 + stamp + slip*2)
        annual_cost = daily_cost * 252
        return annual_cost
    
    def full_audit(self):
        print("="*50)
        print("回测质量审计报告")
        print("="*50)
        
        lb = self.check_lookahead_bias()
        print(f"
[未来函数检查] 发现 {len(lb)} 个问题")
        for issue in lb:
            print(f"  - {issue}")
        
        sb = self.check_survivorship_bias()
        print(f"
[幸存者偏差检查] 发现 {len(sb)} 个问题")
        for issue in sb:
            print(f"  - {issue}")
        
        cost = self.estimate_costs()
        print(f"
[成本估算] 年化交易成本约: {cost:.2%}")
        print("="*50)

# 使用示例
# auditor = BacktestAuditor(price_df, signal_df, trades_df)
# auditor.full_audit()

本章测验

测测你的理解
1. 以下哪项是典型的未来函数?
  • A. 用当日收盘价计算信号并在当日收盘执行交易
  • B. 用昨日收盘价计算信号,今日开盘执行交易
  • C. 用过去20日均价计算信号
  • D. 使用历史财报数据(公告日之后)选股
收盘前你不可能知道收盘价,因此用当日收盘价作为交易信号和执行价格是典型的未来函数。
2. 幸存者偏差会导致回测收益被怎样扭曲?
  • A. 高估——因为失败的股票被从数据集中删除了
  • B. 低估——因为删除了表现好的股票
  • C. 没有影响——幸存者偏差是伪概念
  • D. 影响方向不确定
回测时如果只保留当前还活着的股票,那些退市、破产、ST的股票被删除,策略看起来总是选中好股票,收益被系统性高估。
3. 策略有15个参数,回测了3年历史数据,这最大的风险是什么?
  • A. 计算太慢
  • B. 极高的过拟合风险——参数太多而数据太少
  • C. 无法获取历史数据
  • D. 策略容量太小
参数越多,越容易在历史数据上找到纯属巧合的最优组合。15个参数在3年数据上优化,几乎必然过拟合。

下一章预告

我们已经知道了回测的六大陷阱,但有一个陷阱特别重要、特别隐蔽,值得单独用一整章来讨论——那就是过拟合

第23章将深入探讨过拟合的本质:为什么你的策略在回测中战无不胜,却在实盘中一败涂地?我们将学习如何量化过拟合风险(CSCV方法)、如何用组合对称交叉验证检测过拟合、以及如何设计对过拟合免疫的策略结构。

如果说回测是试衣间,过拟合就是哈哈镜——它让你看到的自己比真实更瘦、更美、更成功。

觉得有收获?分享给同样对量化交易感兴趣的朋友

上一章
过拟合
当回测战神遇到实盘,为什么策略总会失效
下一章
过拟合
当回测战神遇到实盘,为什么策略总会失效

如果这本书对你有所启发,欢迎支持我继续创作

赞赏二维码