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 其实就三步:
- 训练窗口(In-Sample):选取一段历史数据,训练模型参数
- 测试窗口(Out-of-Sample):用训练好的模型,预测紧接着的下一个时间段
- 滚动前进:把训练窗口和测试窗口整体向前移动,重复1和2
我习惯把训练窗口设成测试窗口的3到5倍。比如测试窗口是1个月,训练窗口就设3到5个月。为什么?因为太短的训练窗口学不到足够规律,太长的窗口又可能包含过时的市场结构。
这里有个避坑指南:我曾经把训练窗口设得和测试窗口一样长,结果模型过拟合得一塌糊涂。后来才明白,训练窗口必须足够大,才能捕捉到稳定的统计特征。
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)
这个框架虽然简单,但已经包含了核心逻辑。你想想看,它模拟了真实交易中「每过一段时间就重新训练模型」的过程。
不过,实际项目中我会加一些改进:
- 重叠窗口:有时候测试窗口和下一个训练窗口有部分重叠,这样模型更新更平滑
- 模型保存:每个窗口的模型都保存下来,方便后续分析模型参数的变化
- 性能监控:记录每个测试窗口的夏普比率、最大回撤等指标
4.4 框架结构图
下面我用一张SVG图来展示整个框架的流程,这样你看起来更直观:
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