ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

用Python实现量化策略过拟合检测从样本内外到WalkForward全流程

2026/8/13 11:40:44 拓冰建站 浏览量
用Python实现量化策略过拟合检测从样本内外到WalkForward全流程

用 Python 实现量化策略过拟合检测:从样本内外到 Walk-Forward 全流程

做量化这几年踩过的最大坑,就是"回测漂亮策略实盘全崩"。后来我把所有策略都加了一层"过拟合审计",实盘表现才稳定下来。今天把整个审计流程用 Python 写出来。

我习惯把所有数据都搬到本地,本地数据用的就是 ig50,跑样本外测试、Walk-Forward 这些都非常方便,几十 GB 数据本地查询,不依赖任何 API。下面是具体实现。

一、为什么需要"策略审计"

我之前写过一个回测胜率 62% 的策略,回测曲线看着很舒服。结果实盘跑了 3 个月,胜率跌到 38%——比扔硬币还差。

复盘原因,主要是 5 个过拟合信号全占了:参数敏感、规则复杂、交易次数少、用了未来信息、只在特定区间有效。

从那之后,我强制所有策略上实盘前必须走 6 步审计。这一套跑下来,47 个朋友的策略样本外 alpha 率只有 14.9%——剩下 85% 都是过拟合产物。

今天把这套审计流程完整 Python 化。

二、整体代码架构

importpandasaspdimportnumpyasnpfromtypingimportList,Dict,TupleclassStrategyAuditor:"""策略过拟合审计器"""def__init__(self,strategy_func,data:pd.DataFrame):""" Args: strategy_func: 策略函数,输入 (df, params) 返回交易信号 data: 本地股票数据 DataFrame """self.strategy=strategy_func self.data=data self.results={}defrun_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-Forwardresults['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)returnresults

四、参数稳定性测试

参数敏感度过高 = 过拟合的强信号。我把每个参数 ±20% 浮动,看回测结果波动。

deftest_param_stability(self,base_params:Dict,fluctuation:float=0.2)->Dict:""" 测试参数稳定性 把每个参数上下浮动 ±20%,看回测胜率波动 """stability_results={}base_winrate=self._run_backtest(self.data,base_params)['winrate']forparam_name,base_valueinbase_params.items():param_fluctuations=[]# 测试 -20%, -10%, +10%, +20%forratioin[-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']forrinstability_results.values())return{'all_stable':all_stable,'details':stability_results,'base_winrate':base_winrate}

参数稳定性测试的核心是"参数微调对结果的影响"。如果某个参数稍微动一下胜率就跌 10 个点以上,99% 是过拟合

五、跨区间测试

策略只在特定市场风格区间有效,是过拟合的另一大信号。我至少要用 3 个不同区间测试。

deftest_cross_regime(self,base_params:Dict)->Dict:""" 跨区间测试:牛市区间、熊市区间、震荡区间 """regimes=self._split_by_regime(self.data)regime_results={}forregime_name,regime_datainregimes.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(1forrinregime_results.values()ifr['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={}forregimein['bull','bear','sideways']:regime_data=data[data['regime']==regime].copy()iflen(regime_data)>100:# 至少 100 个交易日regimes[regime]=regime_datareturnregimes

跨区间测试的核心是"策略在不同市场风格下都有效"。如果只在某个区间有效,直接放弃

六、样本外测试

样本外测试是判断过拟合的核心。我用 7:3 时间切分,前 70% 训练、后 30% 测试。

deftest_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']iftrain_result['winrate']>0else0return{'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 年。

deftest_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 个交易日foriinrange(n_rolls):# 每次往前推 1 年end_idx=len(self.data)-i*252train_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]iflen(test_data)<50:continuetest_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(1forrinwalk_forward_resultsifr['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 个点),大概率也是过拟合。

deftest_random_benchmark(self,base_params:Dict)->Dict:""" 随机基准对比 """# 跑 1000 次随机买入持有 20 天的基准策略random_winrates=[]for_inrange(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 步审计全部通过后,给一个综合评分。

defcalc_overall_score(self,results:Dict)->Dict:"""综合评分"""score=0max_score=100# Step 1: 参数稳定性 (20 分)ifresults['param_stability']['all_stable']:score+=20else:stable_count=sum(1forrinresults['param_stability']['details'].values()ifr['is_stable'])score+=stable_count*4# 每个稳定参数 4 分# Step 2: 跨区间测试 (15 分)ifresults['cross_regime']['is_qualified']:score+=15# Step 3: 样本外测试 (25 分)ifresults['out_of_sample']['is_excellent']:score+=25elifresults['out_of_sample']['is_qualified']:score+=18# Step 4: Walk-Forward (25 分)ifresults['walk_forward']['is_qualified']:score+=25# Step 5: 随机基准对比 (15 分)ifresults['random_benchmark']['is_excellent']:score+=15elifresults['random_benchmark']['is_qualified']:score+=10return{'score':score,'grade':'A'ifscore>=80else'B'ifscore>=65else'C'ifscore>=50else'D','is_recommended':score>=65,# B 级以上建议上实盘'is_excellent':score>=80# A 级优秀}

十、实盘验证流程

综合评分 ≥ 65 分(B 级以上)的策略,才允许进入实盘验证:

defreal_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.8returnTrue# 简化版

十一、总结

这套 6 步策略审计流程,是我过去 2 年用真金白银换来的经验:

  1. 参数稳定性测试:参数敏感度过高 = 99% 过拟合
  2. 跨区间测试:只在某个区间有效 = 直接放弃
  3. 样本外测试:样本内 / 样本外胜率比值 < 0.55 = 严重过拟合
  4. 滚动 Walk-Forward:12 次中至少 8 次合格才算合格
  5. 随机基准对比:胜率优势 < 5 个点 = 大概率过拟合
  6. 实盘小资金验证: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