量化交易回测完全指南:从数据准备到策略验证
先说结论
一个策略在回测中表现越完美,越值得怀疑。
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
回测框架选择
数据清干净之后,选一个合适的框架。不同框架的取舍很直接:
| 框架 | 语言 | 优势 | 劣势 | 适合人群 |
|---|---|---|---|---|
| Backtrader | Python | 功能全面,文档详细 | 速度较慢 | Python用户,策略复杂 |
| Zipline | Python | Quantopian官方,社区大 | 不再维护 | 学习用途 |
| VectorBT | Python | 速度极快(向量化) | 灵活性差 | 简单策略,追求速度 |
| 聚宽/米筐 | Python | 数据完整,云端运行 | 收费,依赖平台 | 不想自己处理数据 |
| TradingAgents | Python | 多智能体,准确率高 | 需要学习成本 | 追求高准确率 |
新手、数据处理能力有限,选聚宽/米筐;策略逻辑复杂、需要完全控制,选 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%):最后验证,只能用一次
规则:
- 测试集在策略完全定型前不能碰
- 如果测试集表现不佳,不能回去调整策略
一旦打开测试集,调整过策略再跑,这次验证就没意义了。
回测性能指标
核心指标解读
| 指标 | 计算公式 | 优秀标准 | 说明 |
|---|---|---|---|
| 年化收益率 | (期末/期初)^(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》相关论文