第九章:过拟合与稳健性检验

做策略回测最怕什么?

不是亏钱,是回测曲线漂亮得像假的一样。

我见过太多新手,拿着一个年化80%、回撤不到5%的策略兴奋得睡不着。结果实盘一跑,三个月亏掉一半。嗯,这就是典型的过拟合——你的策略不是在捕捉市场规律,而是在「记住」历史数据里的噪音。

9.1 过拟合:回测的「照骗」

说白了,过拟合就是你的策略太「聪明」了。聪明到把随机波动都当成了规律来学习。

我在做市商策略里遇到过最典型的例子:有人把参数优化到极致,每个品种、每个时间段都用不同的参数。回测曲线完美得像个艺术品。但换到样本外数据,直接崩盘。

过拟合的典型症状:

  • 回测收益极高,但样本外表现极差
  • 参数稍微一改,结果天差地别
  • 策略在特定时间段表现特别好,其他时间一般
  • 交易频率异常高,或者持仓时间异常短

为什么会这样?因为市场里有大量的随机噪声。你想想看,如果你用100个参数去拟合一段行情,总能把曲线画得漂漂亮亮。但这就像用高阶多项式去拟合5个点——数学上完美,预测上完蛋。

9.2 交叉验证:给策略做「体检」

交叉验证不是机器学习专属的。做策略回测一样要用。

我个人习惯用K折交叉验证。把数据切成K份,轮流用K-1份训练、1份验证。这样能看出策略在不同数据切片上的稳定性。

# 一个简单的K折交叉验证框架
import numpy as np
import pandas as pd

def k_fold_backtest(data, strategy_func, k=5):
    """
    data: 完整的历史数据
    strategy_func: 策略函数,输入训练数据,输出交易信号
    k: 折数
    """
    fold_size = len(data) // k
    results = []
    
    for i in range(k):
        # 划分训练集和验证集
        val_start = i * fold_size
        val_end = (i + 1) * fold_size
        
        train_data = pd.concat([data[:val_start], data[val_end:]])
        val_data = data[val_start:val_end]
        
        # 在训练集上优化参数
        params = strategy_func.optimize(train_data)
        
        # 在验证集上测试
        perf = strategy_func.run(val_data, params)
        results.append(perf)
    
    # 看各折结果的稳定性
    sharpe_ratios = [r['sharpe'] for r in results]
    print(f"平均夏普: {np.mean(sharpe_ratios):.2f}")
    print(f"夏普标准差: {np.std(sharpe_ratios):.2f}")
    
    return results

我的经验:K值选5或10比较合适。太小了训练数据不够,太大了验证集太短。我曾经试过K=20,结果每折只有两周数据,波动率估计完全失真。

9.3 滚动回测:更贴近实战的检验

交叉验证有个问题——它假设数据是独立同分布的。但金融数据不是。昨天的行情会影响今天,今天的会影响明天。

所以滚动回测更实用。它模拟了真实的交易场景:用过去的数据训练,在未来的数据上验证,然后窗口往前滚动。

def rolling_backtest(data, strategy_func, window_size=252, step=63):
    """
    滚动回测实现
    window_size: 训练窗口长度(交易日)
    step: 滚动步长
    """
    results = []
    
    for start in range(0, len(data) - window_size, step):
        train_end = start + window_size
        val_end = min(train_end + step, len(data))
        
        train_data = data[start:train_end]
        val_data = data[train_end:val_end]
        
        # 训练和验证
        params = strategy_func.optimize(train_data)
        perf = strategy_func.run(val_data, params)
        
        results.append({
            'train_period': f"{data.index[start]} - {data.index[train_end]}",
            'val_period': f"{data.index[train_end]} - {data.index[val_end]}",
            'sharpe': perf['sharpe'],
            'max_drawdown': perf['max_drawdown']
        })
    
    return pd.DataFrame(results)

注意:滚动窗口的大小要跟你的策略持仓周期匹配。做市商策略通常用3-6个月的窗口。太短了参数不稳定,太长了又跟不上市场变化。

9.4 蒙特卡洛模拟:给策略「压力测试」

蒙特卡洛模拟,说白了就是「如果历史重演,但顺序打乱,你的策略还能赚钱吗?」

我习惯用两种方式做蒙特卡洛:

  1. 收益序列重采样:把历史每日收益率打乱顺序,重新生成多条路径
  2. 参数随机化:在最优参数附近随机采样,看策略的鲁棒性
def monte_carlo_shuffle(returns, n_simulations=1000):
    """
    收益序列重采样蒙特卡洛
    returns: 策略的历史日收益率序列
    """
    results = []
    
    for i in range(n_simulations):
        # 打乱收益率顺序
        shuffled = np.random.permutation(returns)
        
        # 计算累计收益
        cum_returns = np.cumprod(1 + shuffled)
        
        # 计算夏普比率
        sharpe = np.mean(shuffled) / np.std(shuffled) * np.sqrt(252)
        results.append(sharpe)
    
    results = np.array(results)
    
    print(f"原始夏普: {np.mean(returns) / np.std(returns) * np.sqrt(252):.2f}")
    print(f"蒙特卡洛平均夏普: {np.mean(results):.2f}")
    print(f"蒙特卡洛夏普标准差: {np.std(results):.2f}")
    print(f"95%置信区间: [{np.percentile(results, 2.5):.2f}, {np.percentile(results, 97.5):.2f}]")
    
    return results

关键判断标准:

  • 如果蒙特卡洛模拟中,超过20%的路径是亏损的——策略可能过度依赖特定的行情顺序
  • 如果参数随机化后,夏普比率波动超过0.5——参数敏感度过高,容易过拟合
  • 如果95%置信区间包含负值——策略的稳健性存疑

9.5 知识体系总览

下面这张图是我自己总结的过拟合检验流程。每次开发新策略,我都会走一遍这个流程。

过拟合检验与稳健性验证流程 策略开发与参数优化 K折交叉验证 滚动回测检验 蒙特卡洛模拟 是否通过 稳健性检验? 返回优化:简化参数/增加约束 策略通过,进入实盘测试

9.6 实战中的避坑指南

我曾经踩过一个坑,说出来你可能不信——我用滚动回测检验一个做市策略,结果发现每个滚动窗口的夏普都在2.0以上。当时我高兴坏了,觉得发现了圣杯。

后来仔细一看,原来我犯了个低级错误:训练集和验证集之间有数据泄露。我在训练时用了未来信息来计算波动率。嗯,这种错误在回测框架里特别隐蔽,尤其是当你用pandas的shift函数时,一不小心就shift反了方向。

几个实用建议:

  • 做交叉验证时,一定要保证训练集在验证集之前——别用未来的数据预测过去
  • 蒙特卡洛模拟至少跑1000次,太少的话统计意义不大
  • 如果策略在滚动回测中,有超过30%的窗口是亏损的——放弃它,别犹豫
  • 参数敏感性分析:把每个参数在最优值±20%范围内扫描,看策略表现是否稳定

最后说一句:没有哪个策略能通过所有检验。我们的目标不是找到完美的策略,而是找到那些在多种检验下都表现稳健的策略。说白了,就是找那个「怎么折腾都不容易死」的策略。