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

用 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 年用真金白银换来的经验:

  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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值