本章导航
- 学习目标
- 故事引入:曲线拟合的怪物
- 核心知识:过拟合的数学本质与检测方法
- AI实践:用AI检测策略是否过拟合
- 动手练习:CSCV过拟合检测代码
- 本章测验
- 下一章预告
学习目标
过拟合是量化策略的头号杀手。它不像未来函数那样容易被代码审查发现,也不像成本低估那样可以通过简单计算修正。过拟合是一种统计学意义上的幻觉——你看到的规律,可能只是随机噪声的巧合排列。
本章结束后,你将能够:
- 理解过拟合的数学本质:偏差-方差权衡、自由度、多重比较问题
- 掌握CSCV(组合对称交叉验证)方法:量化评估策略的过拟合概率
- 识别过拟合的视觉信号:从参数敏感性图中判断策略是否稳健
- 设计抗过拟合的策略框架:简化模型、正则化、样本外验证的正确打开方式
故事引入:曲线拟合的怪物
19世纪的数学家高斯已经告诉我们,给定N个点,总能找到一条N-1次多项式完美穿过所有点。这意味着如果你允许模型足够复杂,它可以在训练数据上达到100%的准确率——但对新数据毫无预测能力。
2016年,某量化私募发布了一款AI选股产品,号称用深度学习训练了上亿参数。回测显示,2010-2015年年化收益超过80%。产品募资5亿元,2016年实盘运行——结果当年亏损35%,2017年继续亏损20%,基金清盘。
事后分析发现,这个深度学习模型在训练过程中记住了大量历史噪声:某次特定月份的小盘股轮动、某几年的行业周期性规律、甚至某些股票的除权除息日模式。这些模式在未来再也没有重现。
用奥卡姆剃刀的精神来说:如无必要,勿增实体。一个用3个参数就能解释的现象,绝不要用30个参数去拟合。
——威廉·奥卡姆,约1285-1349
核心知识:过拟合的数学本质与检测方法
偏差-方差权衡(Bias-Variance Tradeoff)
任何预测模型的误差都可以分解为三部分:
总误差 = 偏差² + 方差 + 不可约误差
- 偏差(Bias):模型太简单,无法捕捉真实规律。比如用线性模型拟合非线性关系。
- 方差(Variance):模型太复杂,对训练数据的小波动过度敏感。不同的训练样本会产生截然不同的模型。
- 不可约误差:数据本身的随机噪声,任何模型都无法消除。
过拟合的本质就是方差过大——模型记住了训练数据的特有噪声,把这些噪声当成了规律。当遇到新数据时,这些噪声不复存在,模型表现断崖式下跌。
多重比较问题(Multiple Comparison Problem)
假设你测试了100个不同的策略变体,每个变体在5年回测中有5%的概率纯粹因运气而显得显著(p<0.05)。那么, purely by chance,你期望能看到大约5个看起来显著的策略。
更可怕的是,你通常会只汇报最好的那个,而读者根本不知道你还测试了另外99个。这就是量化交易中的出版偏差(Publication Bias)——你只看到幸存者,看不到背后的尸体堆。
flowchart TD
A[提出策略假设] --> B[回测验证]
B --> C{结果显著?}
C -->|是| D[发表/实盘]
C -->|否| E[调整参数]
E --> F[再次回测]
F --> C
D --> G[样本外验证]
G -->|失败| H[过拟合!]
G -->|成功| I[可能稳健]
CSCV(Combinatorially Symmetric Cross-Validation)
CSCV是Marcos Lopez de Prado提出的检测策略过拟合概率的方法。核心思想是:
- 将历史数据分为S个子集(如10个子集)。
- 在所有可能的组合中,选出一半子集作为训练集,另一半作为测试集。
- 对每个划分,在训练集上优化策略参数,在测试集上评估表现。
- 如果训练集最优策略在测试集上表现远差于训练集,说明存在过拟合。
CSCV输出一个过拟合概率(PBO, Probability of Backtest Overfitting)。PBO < 0.1表示策略很可能稳健,PBO > 0.5表示策略很可能过拟合。
import numpy as np
import pandas as pd
from itertools import combinations
def cscv_pbo(returns_matrix, n_splits=10):
"""
CSCV方法计算过拟合概率(PBO)
returns_matrix: DataFrame, 每列是一个策略变体的日收益序列
"""
n_strategies = returns_matrix.shape[1]
T = len(returns_matrix)
# 将收益序列分成n_splits段
splits = np.array_split(returns_matrix, n_splits)
omega = 0 # 过拟合次数计数
n_total = 0
# 所有可能的训练/测试划分(各取一半)
all_indices = list(range(n_splits))
for train_idx in combinations(all_indices, n_splits // 2):
test_idx = [i for i in all_indices if i not in train_idx]
# 拼接训练集和测试集
train_returns = pd.concat([splits[i] for i in train_idx])
test_returns = pd.concat([splits[i] for i in test_idx])
# 在训练集上选择夏普比率最高的策略
train_sharpe = train_returns.mean() / train_returns.std() * np.sqrt(252)
best_idx = train_sharpe.idxmax()
# 计算该策略在测试集上的排名
test_sharpe = test_returns.mean() / test_returns.std() * np.sqrt(252)
test_rank = test_sharpe.rank(ascending=False)[best_idx]
# 如果训练集最优策略在测试集上的排名位于后50%,计为过拟合
if test_rank > n_strategies / 2:
omega += 1
n_total += 1
pbo = omega / n_total if n_total > 0 else 0
return pbo
# 示例:生成10个策略变体的收益序列
np.random.seed(42)
returns = pd.DataFrame({
f'strategy_{i}': np.random.normal(0.0003, 0.02, 2520)
for i in range(10)
})
# 人为让strategy_0在训练期表现更好(添加噪声趋势)
returns['strategy_0'] += np.linspace(0, 0.001, 2520)
pbo = cscv_pbo(returns, n_splits=10)
print(f"过拟合概率 PBO: {pbo:.2%}")
if pbo > 0.5:
print("警告:策略很可能过拟合!")
else:
print("策略看起来比较稳健。")
参数敏感性分析
另一个直观的过拟合检测方法是参数敏感性分析。画出策略收益随关键参数变化的曲线:
- 如果曲线呈现一个宽阔的高原(plateau),说明策略对该参数不敏感,结果稳健。
- 如果曲线呈现一个尖锐的峰值,稍微偏离最优参数收益就暴跌,说明策略过度依赖该参数的特定取值,很可能是过拟合。
与AI一起练习
练习1:让AI分析你的策略参数数量
我的策略有以下可调参数:
- 短期均线周期(5-60日)
- 长期均线周期(60-250日)
- 止损比例(1%-10%)
- 仓位比例(10%-100%)
- 入场阈值(0.5-3个标准差)
请帮我:
1. 计算参数空间的总组合数
2. 评估在10年日频数据上优化这些参数的过拟合风险
3. 建议哪些参数可以固定、哪些可以简化
练习2:让AI实现CSCV
请用Python实现一个完整的CSCV(组合对称交叉验证)框架:
- 输入:N个策略变体的日收益序列(DataFrame)
- 输出:PBO(过拟合概率)、Logit曲线、最优策略在训练集和测试集的排名分布
- 要求:支持自定义分割数(S),绘制可视化图表
- 额外:解释PBO的计算逻辑和统计含义
练习3:让AI设计抗过拟合策略
我想设计一个对过拟合免疫的量化策略框架。
请帮我:
1. 提出3种降低过拟合风险的方法(如正则化、简化模型、贝叶斯优化等)
2. 用Python演示其中一种方法(如LASSO正则化在多因子权重中的应用)
3. 讨论为什么简单策略往往比复杂策略更稳健
动手练习:过拟合检测实战
"""过拟合检测实战:网格搜索 + 参数敏感性分析"""
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
# 模拟策略收益:双均线交叉
def simulate_strategy_returns(short_window, long_window, noise_level=0.02):
np.random.seed(42)
price = 100 * np.exp(np.cumsum(np.random.normal(0.0002, 0.01, 1000)))
short_ma = pd.Series(price).rolling(short_window).mean()
long_ma = pd.Series(price).rolling(long_window).mean()
signal = (short_ma > long_ma).astype(int).diff().fillna(0)
returns = signal.shift(1) * np.diff(price, prepend=price[0]) / price
returns += np.random.normal(0, noise_level, len(returns))
return returns.dropna()
# 网格搜索
results = []
for short in range(5, 61, 5):
for long in range(short+10, 251, 10):
r = simulate_strategy_returns(short, long)
sharpe = r.mean() / r.std() * np.sqrt(252)
results.append({'short': short, 'long': long, 'sharpe': sharpe})
df_results = pd.DataFrame(results)
best = df_results.loc[df_results['sharpe'].idxmax()]
print(f"最优参数: 短期={best['short']}, 长期={best['long']}")
print(f"最优夏普: {best['sharpe']:.2f}")
# 参数敏感性:固定长期均线,看短期均线变化
sensitivity = df_results[df_results['long'] == 120].sort_values('short')
print("
参数敏感性(长期=120):")
print(sensitivity[['short', 'sharpe']].head(10))
# 判断:如果最优参数附近的夏普差异很大,说明过拟合风险高
near_best = df_results[
(df_results['short'].between(best['short']-10, best['short']+10)) &
(df_results['long'].between(best['long']-20, best['long']+20))
]
sharpe_std = near_best['sharpe'].std()
print(f"
最优参数附近夏普标准差: {sharpe_std:.3f}")
if sharpe_std > 0.5:
print("警告:参数敏感,过拟合风险高!")
else:
print("参数较稳健。")
本章测验
下一章预告
我们已经学会了如何检测过拟合,但还有一个更核心的问题:如何在策略设计阶段就避免过拟合?答案是Walk-Forward分析——一种模拟实盘滚动优化的回测方法。
第24章将带你深入Walk-Forward分析的世界。你将学会如何设置训练/测试窗口、如何评估 Walk-Forward 的效率、以及为什么它比传统的全样本优化更接近真实交易场景。
回测优化是开卷考试,Walk-Forward才是闭卷测试。