用 Python 实现量化策略过拟合检测:从样本内外到 Walk-Forward 全流程
做量化这几年踩过的最大坑,就是"回测漂亮策略实盘全崩"。后来我把所有策略都加了一层"过拟合审计",实盘表现才稳定下来。今天把整个审计流程用 Python 写出来。
我习惯把所有数据都搬到本地,本地数据用的就是 ig50,跑样本外测试、Walk-Forward 这些都非常方便,几十 GB 数据本地查询,不依赖任何 API。下面是具体实现。
一、为什么需要"策略审计"
我之前写过一个回测胜率 62% 的策略,回测曲线看着很舒服。结果实盘跑了 3 个月,胜率跌到 38%——比扔硬币还差。
复盘原因,主要是 5 个过拟合信号全占了:参数敏感、规则复杂、交易次数少、用了未来信息、只在特定区间有效。
从那之后,我强制所有策略上实盘前必须走 6 步审计。这一套跑下来,47 个朋友的策略样本外 alpha 率只有 14.9%——剩下 85% 都是过拟合产物。
今天把这套审计流程完整 Python 化。
二、整体代码架构
import pandas as pd
import numpy as np
from typing import List, Dict, Tuple
class StrategyAuditor:
"""策略过拟合审计器"""
def __init__(self, strategy_func, data: pd.DataFrame):
"""
Args:
strategy_func: 策略函数,输入 (df, params) 返回交易信号
data: 本地股票数据 DataFrame
"""
self.strategy = strategy_func
self.data = data
self.results = {}
def run_full_audit(self, base_params: Dict) -> Dict:
"""运行完整 6 步审计"""
results = {}
# Step 1: 参数稳定性测试
results['param_stability'] = self.test_param_stability(base_params)
# Step 2: 跨区间测试
results['cross_regime'] = self.test_cross_regime(base_params)
# Step 3: 样本外测试
results['out_of_sample'] = self.test_out_of_sample(base_params)
# Step 4: 滚动 Walk-Forward
results['walk_forward'] = self.test_walk_forward(base_params)
# Step 5: 随机基准对比
results['random_benchmark'] = self.test_random_benchmark(base_params)
# Step 6: 综合评分
results['overall_score'] = self.calc_overall_score(results)
return results
四、参数稳定性测试
参数敏感度过高 = 过拟合的强信号。我把每个参数 ±20% 浮动,看回测结果波动。
def test_param_stability(self, base_params: Dict, fluctuation: float = 0.2) -> Dict:
"""
测试参数稳定性
把每个参数上下浮动 ±20%,看回测胜率波动
"""
stability_results = {}
base_winrate = self._run_backtest(self.data, base_params)['winrate']
for param_name, base_value in base_params.items():
param_fluctuations = []
# 测试 -20%, -10%, +10%, +20%
for ratio in [-0.2, -0.1, 0.1, 0.2]:
test_params = base_params.copy()
test_params[param_name] = base_value * (1 + ratio)
result = self._run_backtest(self.data, test_params)
param_fluctuations.append(result['winrate'])
# 计算波动范围
fluctuation_range = max(param_fluctuations) - min(param_fluctuations)
stability_results[param_name] = {
'fluctuation_range': fluctuation_range,
'is_stable': fluctuation_range < 5.0, # <5 个点算稳定
'max_winrate': max(param_fluctuations),
'min_winrate': min(param_fluctuations)
}
all_stable = all(r['is_stable'] for r in stability_results.values())
return {
'all_stable': all_stable,
'details': stability_results,
'base_winrate': base_winrate
}
参数稳定性测试的核心是"参数微调对结果的影响"。如果某个参数稍微动一下胜率就跌 10 个点以上,99% 是过拟合。
五、跨区间测试
策略只在特定市场风格区间有效,是过拟合的另一大信号。我至少要用 3 个不同区间测试。
def test_cross_regime(self, base_params: Dict) -> Dict:
"""
跨区间测试:牛市区间、熊市区间、震荡区间
"""
regimes = self._split_by_regime(self.data)
regime_results = {}
for regime_name, regime_data in regimes.items():
result = self._run_backtest(regime_data, base_params)
regime_results[regime_name] = {
'winrate': result['winrate'],
'is_profitable': result['total_return'] > 0
}
# 至少 2 个区间有效才算合格
effective_count = sum(1 for r in regime_results.values() if r['is_profitable'])
return {
'effective_count': effective_count,
'is_qualified': effective_count >= 2,
'details': regime_results
}
def _split_by_regime(self, data: pd.DataFrame) -> Dict[str, pd.DataFrame]:
"""
按市场风格切分数据
这里用沪深 300 的 20 日均线方向作为风格判断依据
"""
data = data.copy()
data['hs300_ma20'] = data['hs300_close'].rolling(20).mean()
data['regime'] = np.where(
data['close'] > data['hs300_ma20'], 'bull',
np.where(data['close'] < data['hs300_ma20'] * 0.95, 'bear', 'sideways')
)
regimes = {}
for regime in ['bull', 'bear', 'sideways']:
regime_data = data[data['regime'] == regime].copy()
if len(regime_data) > 100: # 至少 100 个交易日
regimes[regime] = regime_data
return regimes
跨区间测试的核心是"策略在不同市场风格下都有效"。如果只在某个区间有效,直接放弃。
六、样本外测试
样本外测试是判断过拟合的核心。我用 7:3 时间切分,前 70% 训练、后 30% 测试。
def test_out_of_sample(self, base_params: Dict, train_ratio: float = 0.7) -> Dict:
"""
样本外测试
按时间切 7:3,前 70% 训练、后 30% 测试
"""
split_idx = int(len(self.data) * train_ratio)
train_data = self.data.iloc[:split_idx]
test_data = self.data.iloc[split_idx:]
# 样本内测试
train_result = self._run_backtest(train_data, base_params)
# 样本外测试
test_result = self._run_backtest(test_data, base_params)
# 计算样本内 / 样本外胜率比值
winrate_ratio = test_result['winrate'] / train_result['winrate'] if train_result['winrate'] > 0 else 0
return {
'train_winrate': train_result['winrate'],
'test_winrate': test_result['winrate'],
'winrate_ratio': winrate_ratio,
'is_qualified': winrate_ratio > 0.7, # 比值 > 0.7 算合格
'is_excellent': winrate_ratio > 0.85, # 比值 > 0.85 算优秀
'decline_points': train_result['winrate'] - test_result['winrate']
}
样本外测试的核心是"样本内 / 样本外胜率比值":
- 比值 > 0.85:优秀
- 比值 0.70-0.85:良好
- 比值 0.55-0.70:及格
- 比值 < 0.55:不及格(严重过拟合)
七、滚动 Walk-Forward 测试
单次样本外测试可能运气好或运气差。至少要做 12 次滚动 Walk-Forward,每次往前推 1 年。
def test_walk_forward(self, base_params: Dict, n_rolls: int = 12) -> Dict:
"""
滚动 Walk-Forward 测试
每次往前推 1 年,总共跑 12 次
"""
walk_forward_results = []
total_years = len(self.data) / 252 # 假设一年 252 个交易日
for i in range(n_rolls):
# 每次往前推 1 年
end_idx = len(self.data) - i * 252
train_end = end_idx - int((end_idx - int(total_years * 0.7 * 252)) * 0.3)
train_data = self.data.iloc[:train_end]
test_data = self.data.iloc[train_end:end_idx]
if len(test_data) < 50:
continue
test_result = self._run_backtest(test_data, base_params)
walk_forward_results.append({
'roll': i + 1,
'test_winrate': test_result['winrate'],
'test_return': test_result['total_return']
})
# 统计样本外胜率 > 50% 的次数
qualified_count = sum(1 for r in walk_forward_results if r['test_winrate'] > 50)
return {
'qualified_count': qualified_count,
'total_rolls': len(walk_forward_results),
'is_qualified': qualified_count >= 8, # 12 次中至少 8 次合格
'details': walk_forward_results
}
八、随机基准对比
判断策略是否真的有 alpha,要跟"随机买入持有 20 天"对比。如果只是稍微好一点(< 5 个点),大概率也是过拟合。
def test_random_benchmark(self, base_params: Dict) -> Dict:
"""
随机基准对比
"""
# 跑 1000 次随机买入持有 20 天的基准策略
random_winrates = []
for _ in range(1000):
random_signals = self._generate_random_signals(hold_days=20)
random_result = self._simulate(self.data, random_signals)
random_winrates.append(random_result['winrate'])
# 跑真实策略
strategy_result = self._run_backtest(self.data, base_params)
# 计算胜率优势
advantage = strategy_result['winrate'] - np.mean(random_winrates)
return {
'strategy_winrate': strategy_result['winrate'],
'random_avg_winrate': np.mean(random_winrates),
'advantage': advantage,
'is_qualified': advantage > 5.0, # 胜率优势 > 5 个点算合格
'is_excellent': advantage > 10.0
}
九、综合评分
6 步审计全部通过后,给一个综合评分。
def calc_overall_score(self, results: Dict) -> Dict:
"""综合评分"""
score = 0
max_score = 100
# Step 1: 参数稳定性 (20 分)
if results['param_stability']['all_stable']:
score += 20
else:
stable_count = sum(1 for r in results['param_stability']['details'].values() if r['is_stable'])
score += stable_count * 4 # 每个稳定参数 4 分
# Step 2: 跨区间测试 (15 分)
if results['cross_regime']['is_qualified']:
score += 15
# Step 3: 样本外测试 (25 分)
if results['out_of_sample']['is_excellent']:
score += 25
elif results['out_of_sample']['is_qualified']:
score += 18
# Step 4: Walk-Forward (25 分)
if results['walk_forward']['is_qualified']:
score += 25
# Step 5: 随机基准对比 (15 分)
if results['random_benchmark']['is_excellent']:
score += 15
elif results['random_benchmark']['is_qualified']:
score += 10
return {
'score': score,
'grade': 'A' if score >= 80 else 'B' if score >= 65 else 'C' if score >= 50 else 'D',
'is_recommended': score >= 65, # B 级以上建议上实盘
'is_excellent': score >= 80 # A 级优秀
}
十、实盘验证流程
综合评分 ≥ 65 分(B 级以上)的策略,才允许进入实盘验证:
def real_money_validation(self, strategy, params: Dict, validation_months: int = 3, capital_ratio: float = 0.05) -> bool:
"""
实盘验证
Args:
validation_months: 验证期(月)
capital_ratio: 资金占比
"""
# 用 5% 资金实盘 3 个月
# 3 个月实盘胜率 > 回测胜率 * 0.8 才允许加到 30% 仓位
backtest_result = self._run_backtest(self.data, params)
backtest_winrate = backtest_result['winrate']
# 实盘 3 个月后看真实胜率
# 这里要接入实盘账户数据
# real_winrate = ...
# real_winrate / backtest_winrate > 0.8 才算通过
# return real_winrate / backtest_winrate > 0.8
return True # 简化版
十一、总结
这套 6 步策略审计流程,是我过去 2 年用真金白银换来的经验:
- 参数稳定性测试:参数敏感度过高 = 99% 过拟合
- 跨区间测试:只在某个区间有效 = 直接放弃
- 样本外测试:样本内 / 样本外胜率比值 < 0.55 = 严重过拟合
- 滚动 Walk-Forward:12 次中至少 8 次合格才算合格
- 随机基准对比:胜率优势 < 5 个点 = 大概率过拟合
- 实盘小资金验证:3 个月实盘胜率 > 回测胜率 * 0.8
47 个策略按这套流程跑下来,只有 14.9% 是真 alpha。85% 的"高胜率策略"在实盘都会失效——这就是过拟合审计的价值。
回测胜率只是入场券,样本外表现才是真考试。
接口说明
策略审计模块依赖的核心数据接口(本地数据用的 ig50):
- 行情数据:time/history/trade/{dm}/{级别}
- 财务数据:time/f10/fi/{dm}
- 沪深 300 指数:time/history/trade/000300.SH/{级别}
- 涨跌停数据:time/data/limit_up
字段简写:dm = 股票代码,cjsj = 成交时间,cjjg = 成交价格,cjl = 成交量。
资料参考:ig50

1679

被折叠的 条评论
为什么被折叠?



