返回博客列表
量化交易回测完全指南:从数据准备到策略验证

量化交易回测完全指南:从数据准备到策略验证

实战教程
TradingAgents 研究团队
量化交易回测 策略验证 过拟合 回测陷阱 数据准备

先说结论

一个策略在回测中表现越完美,越值得怀疑。

80% 的量化策略回测亮眼,实盘后失效。原因集中在四个陷阱:过拟合、幸存者偏差、未来函数、交易成本低估。本文不讲理论,只讲每个陷阱对应的真实事故,以及可以直接用的检查清单和代码。


为什么”完美回测”是危险信号

2025 年,某私募基金花了半年开发出一套量化策略:回测年化收益 42%,最大回撤 6%,夏普比率 2.8。数据漂亮到团队没人提出异议,直接投入 2000 万实盘。

三个月后,净值跌去 18%,被迫清盘。

事后复盘,三个错误导致这场灾难:策略用当天收盘价判断买入信号,但收盘前根本不知道收盘价;回测忽略了佣金和滑点,实际每笔交易要多损耗 0.3%~0.8%;测试数据只包含活到现在的股票,已退市的暴雷股全过滤掉了。

三个问题叠加,42% 完全是虚构的。

清华大学金融科技实验室 2025 年研究了 100 个声称”回测年化收益 >30%“的策略,实盘后能维持 >15% 的不足 12 个。

回测的价值是找漏洞,不是证明策略有多好。

回测四大致命陷阱:过拟合、幸存者偏差、未来函数、交易成本


数据准备:垃圾进,垃圾出

1. 数据质量检查

停牌数据:被忽视的陷阱

2024 年,某量化团队测试一个”低波动套利”策略,回测年化收益 35%。实盘后才发现,策略标的里有一批股票在回测期间频繁停牌。停牌期间数据源给的是前收盘价,价格看起来一动不动,策略把这当成”低波动优质标的”大量建仓。

复牌后,这些股票往往跳空大幅波动,策略完全失效。

检查项问题解决方案
停牌数据停牌期间价格不变,误导策略标记停牌日期,回测时跳过
复权处理分红、送股导致价格跳空使用后复权价格
涨跌停回测假设能成交,实际买不到标记涨跌停,限制成交
成交量小盘股成交量不足检查日均成交额,过滤流动性差的股票
数据缺失某些日期数据缺失向前填充或标记为无效

数据清洗代码示例(Python):

import pandas as pd

def clean_stock_data(df):
    """清洗股票数据"""
    # 1. 删除停牌日期
    df = df[df['volume'] > 0]

    # 2. 标记涨跌停
    df['is_limit_up'] = (df['close'] >= df['pre_close'] * 1.095)
    df['is_limit_down'] = (df['close'] <= df['pre_close'] * 0.905)

    # 3. 过滤流动性差的股票(日均成交额<5000万)
    df['turnover'] = df['close'] * df['volume']
    df = df[df['turnover'] > 50_000_000]

    # 4. 处理缺失值
    df = df.fillna(method='ffill')  # 向前填充

    return df

2. 幸存者偏差:最隐蔽的陷阱

2023 年,某团队用”连续 5 年 ROE >15%“的选股条件,在 2018-2023 年数据上回测,年化收益 38%,最大回撤 12%。

看起来完美。问题是:他们的股票列表用的是 2023 年的快照。2018-2022 年间那些退市、变 ST、暂停上市的股票,很多曾经 ROE 同样亮眼,后来暴雷,一个都没测到。

加入这些已退出的股票后:年化收益从 38% 跌到 19%,最大回撤从 12% 升至 34%。

核心原则:回测哪一年,就用哪一年的股票池。具体来说:

  • 用 2018 年的股票列表回测 2018 年,用 2019 年的列表回测 2019 年
  • 数据源要包含退市、ST 股票的完整历史
  • 免费数据源通常只有当前存活股票,需要特别注意

检查方法

# 检查是否存在幸存者偏差
def check_survivorship_bias(stock_list_2018, stock_list_2023):
    """检查2018年的股票中有多少在2023年仍存在"""
    survived = set(stock_list_2018) & set(stock_list_2023)
    survival_rate = len(survived) / len(stock_list_2018)

    print(f"2018年股票数量: {len(stock_list_2018)}")
    print(f"2023年仍存在: {len(survived)}")
    print(f"存活率: {survival_rate:.1%}")

    if survival_rate > 0.95:
        print("⚠️ 警告:存活率过高,可能存在幸存者偏差")

    return survival_rate

# 示例输出:
# 2018年股票数量: 3547
# 2023年仍存在: 3201
# 存活率: 90.2%
# ⚠️ 警告:存活率过高,可能存在幸存者偏差

3. 未来函数:最致命的错误

某团队开发了一个”日内反转”策略:如果当天收盘价 < 开盘价,则在收盘前买入,第二天开盘卖出。

回测胜率 68%,年化收益 29%。实盘后发现根本无法执行——买入条件是”当天收盘价低于开盘价”,但收盘前不知道收盘价,信号永远无法触发。

用未来时刻的信息做当前决策,是这类错误的本质,而且往往很隐蔽,写代码时自己没察觉。

错误类型示例正确做法
使用当天收盘价用当天收盘价判断是否买入用前一天收盘价或当天开盘价
使用未来财报用Q2财报(5月发布)回测4月财报发布后才能使用
使用调整后数据用后复权价格计算历史收益使用前复权或不复权价格
使用未来排名用全年涨幅排名选股只能用历史数据排名

检查代码

def check_future_function(df, signal_col, price_col):
    """检查是否存在未来函数"""
    # 检查信号是否使用了当天价格
    df['signal_shift'] = df[signal_col].shift(1)

    # 如果信号和价格在同一天,可能存在未来函数
    correlation = df[signal_col].corr(df[price_col])

    if abs(correlation) > 0.3:
        print(f"⚠️ 警告:信号与当天价格相关性{correlation:.2f},可能存在未来函数")
        return True

    return False

回测框架选择

数据清干净之后,选一个合适的框架。不同框架的取舍很直接:

框架语言优势劣势适合人群
BacktraderPython功能全面,文档详细速度较慢Python用户,策略复杂
ZiplinePythonQuantopian官方,社区大不再维护学习用途
VectorBTPython速度极快(向量化)灵活性差简单策略,追求速度
聚宽/米筐Python数据完整,云端运行收费,依赖平台不想自己处理数据
TradingAgentsPython多智能体,准确率高需要学习成本追求高准确率

新手、数据处理能力有限,选聚宽/米筐;策略逻辑复杂、需要完全控制,选 Backtrader;大规模参数扫描、跑速度,选 VectorBT;有完全定制化需求,自建框架。

回测框架的核心功能

一个合格的框架要有六件事:事件驱动(按时间顺序,杜绝未来函数)、订单管理(模拟真实成交流程)、滑点模拟、佣金计算、风险管理、性能分析。

简化的回测框架示例

class SimpleBacktest:
    def __init__(self, initial_capital=1000000):
        self.capital = initial_capital
        self.positions = {}  # 持仓
        self.trades = []  # 交易记录

    def buy(self, stock, price, shares, date):
        """买入"""
        cost = price * shares * (1 + 0.0003)  # 佣金0.03%
        if cost > self.capital:
            return False  # 资金不足

        self.capital -= cost
        self.positions[stock] = {
            'shares': shares,
            'cost': price,
            'date': date
        }
        self.trades.append({
            'date': date,
            'stock': stock,
            'action': 'buy',
            'price': price,
            'shares': shares
        })
        return True

    def sell(self, stock, price, date):
        """卖出"""
        if stock not in self.positions:
            return False

        shares = self.positions[stock]['shares']
        revenue = price * shares * (1 - 0.0013)  # 佣金0.03% + 印花税0.1%

        self.capital += revenue
        profit = revenue - (self.positions[stock]['cost'] * shares * 1.0003)

        del self.positions[stock]
        self.trades.append({
            'date': date,
            'stock': stock,
            'action': 'sell',
            'price': price,
            'shares': shares,
            'profit': profit
        })
        return True

    def get_total_value(self, current_prices):
        """计算总资产"""
        position_value = sum(
            current_prices.get(stock, 0) * pos['shares']
            for stock, pos in self.positions.items()
        )
        return self.capital + position_value

常见回测陷阱

数据干净、框架选好,仍然可能在这三个陷阱里翻车。

1. 过拟合:回测完美,实盘爆炸

某团队基于 MACD 开发策略,通过穷举优化,在 2018-2023 年数据上找到了”最优参数”:快线 17 天、慢线 38 天、信号线 11 天。回测年化收益 41%,夏普比率 2.6。

2024 年实盘,收益 8%,不如买沪深 300 指数基金。

这组参数是专门为 2018-2023 这段历史量身定做的,每个数字都在拟合已发生的事件。换一段数据,模型完全失效。

识别过拟合有三种方法。第一,样本外测试:数据分 70% 训练集 + 30% 测试集,训练集优化参数,测试集验证,测试集收益 < 训练集 70% 则告警。第二,参数敏感性测试:把参数调整 ±10%,收益波动超 20% 说明策略依赖特定数值,不稳健。第三,时间稳定性测试:按年份分段回测,看是否只在某几年表现突出。

过拟合检测代码

def detect_overfitting(strategy, data, param_range):
    """检测过拟合"""
    # 1. 样本外测试
    train_data = data[:int(len(data) * 0.7)]
    test_data = data[int(len(data) * 0.7):]

    train_return = strategy.backtest(train_data)
    test_return = strategy.backtest(test_data)

    print(f"训练集收益: {train_return:.1%}")
    print(f"测试集收益: {test_return:.1%}")
    print(f"收益衰减: {(train_return - test_return) / train_return:.1%}")

    if (train_return - test_return) / train_return > 0.3:
        print("⚠️ 警告:测试集收益衰减>30%,可能过拟合")

    # 2. 参数敏感性测试
    returns = []
    for param in param_range:
        strategy.set_param(param)
        ret = strategy.backtest(data)
        returns.append(ret)

    std = np.std(returns)
    mean = np.mean(returns)
    cv = std / mean  # 变异系数

    print(f"参数变异系数: {cv:.2f}")
    if cv > 0.5:
        print("⚠️ 警告:参数敏感性过高,策略不稳健")

2. 交易成本低估

某高频策略捕捉 1% 以内的价格波动,日均交易 50 次,回测年化收益 32%。

实盘后,每次成交价格比预期差 0.1%~0.2%(滑点),50 次 × 0.15% = 7.5% 日损耗。一个月后策略亏损 18%。

A 股一次完整买卖的真实成本:

成本类型A股费率说明
佣金0.02-0.03%双向收取,最低5元
印花税0.1%仅卖出时收取
过户费0.001%双向收取
滑点0.1-0.5%取决于流动性和交易量
冲击成本0.05-0.2%大单对价格的影响
总计0.3-1.0%单次买卖的总成本

滑点是最容易被低估的一项,大单和小盘股尤其严重,实际成交价格可以比预期差很多。

滑点模拟

def simulate_slippage(price, volume, order_size, volatility):
    """模拟滑点"""
    # 基础滑点:0.1%
    base_slippage = 0.001

    # 流动性滑点:订单占成交量比例
    liquidity_slippage = (order_size / volume) * 0.01

    # 波动性滑点:市场波动越大,滑点越大
    volatility_slippage = volatility * 0.5

    total_slippage = base_slippage + liquidity_slippage + volatility_slippage

    # 买入时价格更高,卖出时价格更低
    return total_slippage

3. 数据窥探偏差

某团队开发策略的过程:第一版回测收益 18%,不满意,调参后 25%,加过滤条件后 32%,再优化到 38%。

每一步调整都参考了同一份历史数据。最终 38% 的回测收益,本质是把这份数据的规律背下来了,不是预测能力。

解决方案是三段式验证,且测试集只能用一次:

数据分割:
- 训练集(50%):用于开发和优化策略
- 验证集(25%):用于调整参数
- 测试集(25%):最后验证,只能用一次

规则:
- 测试集在策略完全定型前不能碰
- 如果测试集表现不佳,不能回去调整策略

一旦打开测试集,调整过策略再跑,这次验证就没意义了。

三段式验证流程:训练集50%→验证集25%→测试集25%→实盘验证


回测性能指标

核心指标解读

指标计算公式优秀标准说明
年化收益率(期末/期初)^(1/年数) - 1> 20%绝对收益能力
最大回撤(峰值-谷值)/峰值< 15%风险控制能力
夏普比率(收益-无风险利率)/波动率> 1.5风险调整后收益
胜率盈利次数/总次数> 55%策略稳定性
盈亏比平均盈利/平均亏损> 2.0单笔交易质量
卡玛比率年化收益/最大回撤> 2.0收益回撤比

年化收益 50%+ 但最大回撤 40%+,风险远超收益;胜率 90%+ 但盈亏比 0.5,赚小亏大;夏普比率 3.0+,多半是过拟合。

高胜率的陷阱

某策略回测:胜率 88%,年化收益 35%。看起来很稳。

但细看单笔数据:平均盈利 +2.1%,平均亏损 -15.3%,盈亏比 0.14。

100 次里赚 88 次,但那 12 次亏损足以吃掉全部利润。2024 年某次黑天鹅事件,策略单日亏损 22%,全年收益归零。

盈亏比过低时,胜率越高,账面上的收益积累越快,但一次大亏就清零,反而更危险。


回测检查清单

实盘前,逐项过一遍这 18 条。

数据质量(5项)

  • 数据包含停牌、涨跌停标记
  • 使用后复权价格
  • 过滤流动性差的股票(日均成交额<5000万)
  • 包含退市、ST股票(避免幸存者偏差)
  • 数据时间跨度≥5年,覆盖牛熊市

回测逻辑(6项)

  • 无未来函数(信号只使用历史数据)
  • 考虑交易成本(佣金+印花税+滑点≥0.3%)
  • 模拟真实成交(涨停买不到,跌停卖不出)
  • 仓位管理合理(单股≤20%,总仓位≤80%)
  • 有止损机制(单笔亏损≤5%)
  • 订单延迟(信号产生后至少1分钟才能成交)

性能验证(4项)

  • 样本外测试(测试集收益≥训练集的70%)
  • 参数稳定性(参数±10%,收益波动<20%)
  • 时间稳定性(每年收益波动<50%)
  • 压力测试(极端行情下最大回撤<30%)

实盘准备(3项)

  • 小资金验证(先用10%资金实盘1-3个月)
  • 监控指标(实盘收益与回测偏差<30%)
  • 止损计划(如果连续3个月跑输回测,暂停策略)

TradingAgents回测功能

TradingAgents 的回测模块内置了上述检查机制:事件驱动引擎严格按时间序列处理信号,自动检测未来函数;完整模拟佣金、印花税和真实滑点;使用时点数据而非当前快照,修正幸存者偏差;训练/验证/测试集自动分割。

回测报告示例

策略名称:多智能体选股策略
回测期间:2020-01-01 至 2025-12-31

=== 收益指标 ===
年化收益率:28.3%
累计收益率:187.6%
基准收益率(沪深300):45.2%
超额收益:142.4%

=== 风险指标 ===
最大回撤:-12.7%(2022-04-15)
夏普比率:2.1
卡玛比率:2.2
波动率:13.5%

=== 交易统计 ===
总交易次数:247次
胜率:68.4%
平均盈利:+5.2%
平均亏损:-2.8%
盈亏比:1.86

=== 验证结果 ===
✅ 无未来函数
✅ 样本外测试通过(测试集收益24.1%)
✅ 参数稳定性良好(CV=0.23)
⚠️ 2022年收益-8.3%(熊市表现欠佳)

总结

三件事,不管回测多漂亮都要做:宁可低估收益,不可高估;样本外测试、参数稳定性、时间稳定性,缺一不可;回测再好,先用 10% 仓位实盘跑 1~3 个月。

回测年化 30%,实盘能做到 20% 就算不错了。如果实盘收益和回测高度吻合,反而要想想是不是数据窥探,或者只是运气好。

下一步


免责声明:本文内容仅供研究与教育使用,不构成投资建议。量化交易存在风险,历史业绩不代表未来表现。投资者应根据自身情况谨慎决策,自行承担投资风险。

数据来源

  • 清华大学金融科技实验室《量化回测研究报告》
  • 中国量化投资学会《回测陷阱白皮书》
  • TradingAgents 2020-2025年回测数据
  • 《Journal of Financial Data Science》相关论文