4、滚动训练与回测框架:构建滚动训练-测试循环、walk-forward analysis 原理、代码框架搭建

各位同学,今天我们来聊聊量化策略里一个非常核心的实操问题——滚动训练与回测框架

说实话,我见过太多人做回测时犯一个毛病:拿全部历史数据训练,再拿全部历史数据测试。结果呢?回测曲线漂亮得像假的一样,一上实盘就崩。为什么会这样?因为你无意中用了未来信息。

嗯,这里要引入一个概念——walk-forward analysis(滚动前进分析)。说白了,就是模拟真实交易中「边学边用」的过程。你不可能知道明天的数据,对吧?那训练模型时,就应该只用今天之前的数据。

4.1 为什么需要滚动训练?

我刚开始做量化时,也踩过这个坑。记得有一次,我用2015到2020年的数据训练了一个LSTM模型,回测年化收益40%,最大回撤不到10%。我兴奋得不行,结果实盘跑了两个月,亏了15%。

后来复盘才发现,模型在训练集里「偷看」了2015年股灾后的V型反弹模式。但2021年的市场结构完全变了,模型根本适应不了。

所以,滚动训练的核心逻辑就是:用过去一段窗口的数据训练,预测下一段窗口,然后窗口向前滑动,重复这个过程。你想想看,这就像你每天看过去30天的天气,预测明天的天气,而不是用过去10年的数据去预测明天。

核心要点:滚动训练的本质是「时间序列的交叉验证」,它避免了数据泄露,也更能反映模型在真实环境下的表现。

4.2 Walk-Forward Analysis 原理

walk-forward analysis 其实就三步:

  1. 训练窗口(In-Sample):选取一段历史数据,训练模型参数
  2. 测试窗口(Out-of-Sample):用训练好的模型,预测紧接着的下一个时间段
  3. 滚动前进:把训练窗口和测试窗口整体向前移动,重复1和2

我习惯把训练窗口设成测试窗口的3到5倍。比如测试窗口是1个月,训练窗口就设3到5个月。为什么?因为太短的训练窗口学不到足够规律,太长的窗口又可能包含过时的市场结构。

这里有个避坑指南:我曾经把训练窗口设得和测试窗口一样长,结果模型过拟合得一塌糊涂。后来才明白,训练窗口必须足够大,才能捕捉到稳定的统计特征。

个人经验:对于股票日频数据,我通常用6个月训练 + 1个月测试。对于期货分钟数据,我可能用5天训练 + 1天测试。窗口大小取决于你的策略频率和数据特性。

4.3 代码框架搭建

好,理论讲完了,咱们直接上代码。我习惯用Python写这类框架,因为pandas和numpy处理时间序列太方便了。

先看一个最简版本的滚动训练-测试循环:

import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression

def walk_forward_analysis(data, train_window, test_window, model_class):
    """
    滚动前进分析框架
    data: 包含特征和标签的DataFrame,按时间排序
    train_window: 训练窗口长度(行数)
    test_window: 测试窗口长度(行数)
    model_class: 模型类,需实现fit和predict方法
    """
    predictions = []
    actuals = []
    models = []
    
    start = 0
    while start + train_window + test_window <= len(data):
        # 切分训练集和测试集
        train_data = data.iloc[start:start + train_window]
        test_data = data.iloc[start + train_window:start + train_window + test_window]
        
        # 分离特征和标签
        X_train = train_data.drop('target', axis=1).values
        y_train = train_data['target'].values
        X_test = test_data.drop('target', axis=1).values
        y_test = test_data['target'].values
        
        # 训练模型
        model = model_class()
        model.fit(X_train, y_train)
        
        # 预测
        pred = model.predict(X_test)
        
        # 保存结果
        predictions.extend(pred)
        actuals.extend(y_test)
        models.append(model)
        
        # 滚动前进
        start += test_window
    
    return np.array(predictions), np.array(actuals), models

# 使用示例
# preds, actuals, models = walk_forward_analysis(data, 120, 20, LinearRegression)

这个框架虽然简单,但已经包含了核心逻辑。你想想看,它模拟了真实交易中「每过一段时间就重新训练模型」的过程。

不过,实际项目中我会加一些改进:

  • 重叠窗口:有时候测试窗口和下一个训练窗口有部分重叠,这样模型更新更平滑
  • 模型保存:每个窗口的模型都保存下来,方便后续分析模型参数的变化
  • 性能监控:记录每个测试窗口的夏普比率、最大回撤等指标
注意:滚动训练的计算量很大。如果你的数据有10年日频数据,测试窗口1个月,那就要滚动120次。每次都要重新训练模型。所以,尽量选择训练速度快的模型,或者用增量学习的方法。

4.4 框架结构图

下面我用一张SVG图来展示整个框架的流程,这样你看起来更直观:

滚动训练与回测框架流程图 原始数据 训练窗口 测试窗口 模型训练 模型预测 结果保存 滚动前进(窗口滑动) 训练窗口(In-Sample)→ 训练模型 → 预测测试窗口(Out-of-Sample) 然后整体向前滑动,重复上述过程 建议:训练窗口 = 3~5 × 测试窗口,避免过拟合

4.5 进阶:动态权重与模型融合

在实际项目中,我还会在滚动框架里加入动态权重。什么意思呢?就是每个窗口训练出来的模型,我不直接用它预测,而是根据最近的表现给它打分。

比如,最近3个测试窗口表现好的模型,权重就高一些。表现差的,权重就降低。这样,即使市场风格变了,模型组合也能自适应调整。

代码实现其实不复杂,就是在上面框架的基础上,加一个权重计算函数:

def calculate_weights(models, recent_performance):
    """
    根据近期表现计算模型权重
    recent_performance: 每个模型最近N个测试窗口的夏普比率列表
    """
    # 使用softmax将表现分数转为权重
    exp_scores = np.exp(recent_performance)
    weights = exp_scores / np.sum(exp_scores)
    return weights

# 在滚动循环中,保存每个模型的近期表现
# 然后计算权重,做加权平均预测
一个小技巧:我习惯用「衰减因子」来给不同时期的测试窗口加权。越近的窗口权重越大,越远的权重越小。这样模型对近期市场变化更敏感。

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 数据泄露:确保训练集和测试集在时间上严格不重叠。我曾经因为shift函数用错了,导致测试集里混入了未来数据,回测结果虚高。
  • 窗口大小选择:没有万能窗口。我建议做敏感性分析,试试不同的窗口大小组合,看看结果是否稳定。
  • 计算效率:如果数据量大,可以考虑用并行计算。每个窗口的训练是独立的,可以同时跑。
  • 模型退化:有时候滚动训练到后期,模型参数会漂移得很厉害。我一般会加一个「模型重置」机制,如果连续几个窗口表现太差,就重新初始化模型。

嗯,关于滚动训练与回测框架,今天就讲到这里。这个框架是量化策略从「纸上谈兵」走向「实盘验证」的关键一步。你回去之后,可以拿自己的策略试试看,把回测改成滚动模式,你会发现很多之前没注意到的问题。

记住:回测不滚动,实盘两行泪


公众号:蓝海数据掘金营,微信 deep3321