halo 的技术博客

返回

你看到的夏普比率2.0,可能是幻觉#

“回测是量化交易者的墓志铭。” —— 某不知名宽客

如果你曾经这样做过,恭喜你,你已经掉进了过拟合的陷阱:

  • ✗ 看到回测曲线下滑,调整参数让它变平
  • ✗ 尝试了20个参数组合,选了最好的那个
  • ✗ 用同一组数据做训练+验证+测试
  • ✗ 策略在2015-2020年表现很好,但2021年后崩了

**过拟合(Overfitting)**是量化策略从纸面富裕到实盘破产的罪魁祸首。

什么是过拟合?#

简单定义:策略过度适应历史数据中的噪声,而非捕捉真实规律。

过拟合的三个特征#

  1. 样本内(IS)表现极好,样本外(OOS)表现崩盘

    • 回测夏普比率:2.5
    • 实盘夏普比率:0.3
  2. 参数敏感:参数微小变化导致性能剧烈波动

    参数组合A: 夏普2.0
    参数组合B: 夏普0.5  (只差一个参数值)
    plaintext
  3. 复杂度过高:用了太多变量、太深的模型

    • 用LSTM预测明天股价(99%过拟合)
    • 用10个因子做选股(可能过拟合)

过拟合的数学原理#

偏差-方差权衡(Bias-Variance Tradeoff)#

总误差 = 偏差² + 方差 + 不可约误差
plaintext
  • 高偏差:模型太简单,欠拟合(训练误差大)
  • 高方差:模型太复杂,过拟合(训练误差小,测试误差大)

结论

  • 左图(1次):欠拟合
  • 中图(3次):刚刚好
  • 右图(15次):过拟合(完美拟合噪声)

量化策略中的过拟合陷阱#

陷阱1:参数挖掘(Parameter Mining)#

# ❌ 错误做法:网格搜索找最优参数
best_sharpe = -np.inf
best_params = None

for ma_short in range(5, 50):
    for ma_long in range(50, 200):
        # 回测
        sharpe = backtest(ma_short, ma_long)
        
        if sharpe > best_sharpe:
            best_sharpe = sharpe
            best_params = (ma_short, ma_long)

print(f"最优参数: {best_params}, 夏普: {best_sharpe}")
# 问题:这个夏普是"挖"出来的,不是真实的
python

解决方案:样本外测试

# ✅ 正确做法:训练集+验证集+测试集
train_data = data[:'2020-01-01']
val_data = data['2020-01-01':'2022-01-01']
test_data = data['2022-01-01':]

# 在训练集上优化参数
best_params = optimize(train_data)

# 在验证集上验证
val_sharpe = backtest(best_params, val_data)

# 最终在测试集上评估(只能用一次!)
test_sharpe = backtest(best_params, test_data)
python

陷阱2:多次回测(Multiple Testing)#

如果你尝试了100个策略想法,其中5个在回测中显著(p<0.05),那么这5个可能都是假阳性

家族wise错误率(Familywise Error Rate)

P(至少1个假阳性) = 1 - (1 - α)^n
plaintext

如果α=0.05,n=100:

P = 1 - (0.95)^100 ≈ 0.994
plaintext

99.4%的概率至少有一个假阳性!

解决方案:Bonferroni校正

新阈值 = α / n
plaintext

如果n=100,α=0.05:

新阈值 = 0.05 / 100 = 0.0005
plaintext

陷阱3:前视偏差(Look-Ahead Bias)#

# ❌ 错误:用了未来数据
def calculate_signal(data):
    # 用全样本均值(包含未来数据!)
    mean = data['close'].mean()
    return data['close'] > mean

# ✅ 正确:只用历史数据
def calculate_signal(data):
    # 用滚动窗口均值
    data['mean'] = data['close'].rolling(window=20).mean()
    return data['close'] > data['mean']
python

陷阱4:幸存者偏差(Survivorship Bias)#

如果你只用当前还存在的股票做回测,会高估策略表现。

例子

  • 2000年有1000只股票
  • 2020年只剩600只(400只退市)
  • 只用600只做回测 → 忽略了退市股票的亏损

解决方案:使用包含退市股票的全样本数据(如CRSP、Wind全A股)

检测过拟合的方法#

1. 样本外测试(Out-of-Sample Test)#

2. 参数稳定性测试#

3. White Reality Check(现实检验)#

4. 交叉验证(Time Series Split)#

防止过拟合的最佳实践#

1. 简化模型(Occam’s Razor)#

原则:能用简单模型就不要用复杂模型

# ❌ 复杂:10个因子 + LSTM
features = ['momentum', 'reversal', 'volume', 'volatility', 'skewness', 
            'liquidity', 'beta', 'size', 'value', 'quality']
model = LSTM(units=64)

# ✅ 简单:2-3个核心因子 + 线性回归
features = ['momentum', 'value']
model = LinearRegression()
python

2. 正则化(Regularization)#

from sklearn.linear_model import Ridge, Lasso

# L2正则化(Ridge):惩罚大系数
model_ridge = Ridge(alpha=1.0)  # alpha越大,惩罚越重

# L1正则化(Lasso):自动特征选择
model_lasso = Lasso(alpha=0.1)  # 会把不重要的系数设为0
python

3. 集成学习(Ensemble)#

# 组合多个简单模型,而非用一个复杂模型
models = [
    (linear_model, {'momentum': 0.4, 'value': 0.6}),
    (tree_model, {'momentum': 0.3, 'value': 0.7}),
    (svm_model, {'momentum': 0.5, 'value': 0.5})
]

# 加权平均
final_signal = np.mean([model.predict(data) for model, _ in models], axis=0)
python

4. 样本外验证流程#

┌─────────────┐
│  全样本数据  │
└──────┬──────┘

       ├─ 训练集 (60%) ─→ 优化参数

       ├─ 验证集 (20%) ─→ 调整模型/选择特征

       └─ 测试集 (20%) ─→ 最终评估(只能用一次!)
plaintext

关键

  • 测试集只能用一次(否则就是过拟合测试集)
  • 如果性能不满意,不能回到训练集重新优化

5. 记录所有尝试#

# 记录每次回测(包括失败的)
backtest_log = []

def log_backtest(params, performance, notes=""):
    backtest_log.append({
        'params': params,
        'sharpe': performance['sharpe'],
        'max_dd': performance['max_drawdown'],
        'notes': notes,
        'timestamp': datetime.now()
    })

# 如果尝试了100个参数组合,但只报告最好的1个 → 欺骗!
# 必须报告所有尝试,或至少报告尝试次数
python

实战案例:均线策略的过拟合#

错误做法#

# ❌ 在全部数据上优化参数
best_sharpe = -np.inf
for ma_short in range(5, 50):
    for ma_long in range(50, 200):
        sharpe = backtest_ma_strategy(data, ma_short, ma_long)
        if sharpe > best_sharpe:
            best_sharpe = sharpe
            best_params = (ma_short, ma_long)

print(f"最优参数: {best_params}, 夏普: {best_sharpe}")
# 输出: (12, 67), 夏普2.8 ← 过拟合!
python

正确做法#

更稳健的做法:简化参数#

# ✅✅ 只用1个参数(均线周期)
for ma in [20, 50, 100, 200]:
    train_sharpe = backtest_ma_strategy(train, ma)
    test_sharpe = backtest_ma_strategy(test, ma)
    
    print(f"MA={ma}: 训练夏普={train_sharpe:.2f}, 测试夏普={test_sharpe:.2f}")

# 输出:
# MA=20: 训练夏普=1.5, 测试夏普=1.3 ← 稳定
# MA=50: 训练夏普=1.8, 测试夏普=1.6 ← 稳定
# MA=100: 训练夏普=1.6, 测试夏普=1.5 ← 稳定
# MA=200: 训练夏普=1.4, 测试夏普=1.3 ← 稳定
python

工具推荐#

Python库#

  1. QuantConnect / Lean:提供样本外测试框架
  2. Backtrader:支持滚动窗口优化
  3. empyrical:计算金融指标(夏普、最大回撤等)
  4. alphalens:因子分析(避免数据挖掘)

商业软件#

  • Quantopian (已关闭,但论文值得读)
  • Numerai:使用加密数据防止过拟合
  • WorldQuant Brain:提供样本外测试环境

总结#

过拟合是量化策略失败的头号原因。避免过拟合的关键:

简化模型:从简单开始,逐步增加复杂度
样本外测试:永远保留”没见过”的数据
记录所有尝试:避免选择性报告
参数稳定性:好的策略应对参数不敏感
集成学习:组合多个简单模型

记住

“如果某个策略看起来好得不像真的,那它很可能就不是真的。"
"回测中的夏普比率2.0,实盘可能只有0.5。"
"简单策略 + 严格验证 > 复杂策略 + 完美回测”


本文仅供学习交流,不构成投资建议。量化交易有风险,实盘需谨慎。

回测过拟合:量化策略死亡的99%原因
https://blog.halo26812.eu.org/blog/backtest-overfitting
Author halo
Published at 2026年6月2日
版权声明 CC BY-NC-SA 4.0
Comment seems to stuck. Try to refresh?✨