13、交叉验证与回测:时间序列交叉验证、滚动窗口回测、扩展窗口回测、回测中的常见陷阱

做量化交易的朋友,尤其是搞基差策略的,一定绕不开一个核心问题:你的策略到底靠不靠谱?

很多人跑完回测,看到曲线漂亮得不行,就急着实盘。结果呢?一进去就亏。为什么?因为回测本身就有坑。今天我就把这些坑,一个一个给你指出来。

13.1 为什么普通交叉验证不适合时间序列?

先问个问题:你用过K折交叉验证吗?

传统的K折,是把数据随机打乱,分成K份。然后拿其中K-1份训练,1份验证。这在分类、回归问题上没问题。但用在时间序列上,就出大事了。

为什么?因为时间序列有顺序。你用未来的数据去训练模型,然后用过去的数据去验证——这不就是典型的“未来函数”吗?

我刚开始做策略时,就犯过这个错。当时用了一个简单的移动平均策略,K折验证下来夏普比率2.5,兴奋得不行。结果实盘直接腰斩。后来才发现,我的验证集里包含了未来的价格信息。说白了,就是作弊了。

核心原则:时间序列的交叉验证,必须保持时间顺序。永远不能用未来的数据去预测过去。

13.2 时间序列交叉验证(Time Series Cross-Validation)

那正确的做法是什么?

时间序列交叉验证,也叫“前向链式验证”。它的逻辑很简单:

  • 第一次:用第1天到第100天训练,预测第101天到第110天
  • 第二次:用第1天到第110天训练,预测第111天到第120天
  • 第三次:用第1天到第120天训练,预测第121天到第130天
  • ……以此类推

你看,每次训练集都在扩大,验证集永远在训练集之后。这样就不会有未来信息泄露的问题。

# 时间序列交叉验证示例(伪代码)
from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)
for train_index, test_index in tscv.split(data):
    train_data = data[train_index]
    test_data = data[test_index]
    # 训练模型,验证策略

我个人习惯用5到10折。折数太少,验证结果不稳定;折数太多,计算量太大。嗯,这个平衡点需要你自己根据数据量来调。

13.3 滚动窗口回测(Rolling Window Backtest)

滚动窗口回测,是另一种常见方法。它的特点是:窗口大小固定

比如,我固定用过去60天的数据训练,然后预测未来5天。然后窗口整体往后滚动30天,再用新的60天数据训练,预测接下来的5天。

这样做的好处是:模型能适应市场的变化。毕竟市场风格会变,你总不能指望2015年的规律在2025年还管用吧?

我的经验:滚动窗口的步长很关键。步长太小,回测次数太多,容易过拟合;步长太大,又可能错过市场风格切换的节点。我一般设步长为窗口大小的10%-20%。

13.4 扩展窗口回测(Expanding Window Backtest)

扩展窗口和滚动窗口的区别在于:窗口只扩大,不缩小

第一次用1-100天训练,第二次用1-110天训练,第三次用1-120天训练……训练集越来越大。

这种方法适合那些需要大量历史数据才能稳定的模型。比如一些统计套利模型,数据越多,参数估计越准。

但有个问题:如果市场发生了结构性变化,早期数据反而会拖累模型。比如2015年股灾前的规律,在2016年可能就不适用了。这时候扩展窗口反而不如滚动窗口灵活。

方法 窗口变化 优点 缺点
时间序列交叉验证 逐步扩展 无未来信息泄露 计算量大
滚动窗口回测 固定大小,整体移动 适应市场变化 可能丢失长期规律
扩展窗口回测 只扩大不缩小 数据利用率高 受早期数据影响大

13.5 回测中的常见陷阱

这部分我得多说几句。因为很多新手,甚至一些老手,都会在这里栽跟头。

13.5.1 前视偏差(Look-Ahead Bias)

前视偏差,说白了就是用未来信息做决策

举个例子:你在回测中,用当天的收盘价作为信号,然后当天就开仓。但实际交易中,收盘价出来之前,你根本不知道价格是多少。这就是典型的前视偏差。

我曾经见过一个策略,回测年化收益50%,实盘直接亏30%。后来一查,原来策略里用了“未来5分钟的数据”来计算指标。你说这能赚钱吗?

避坑指南:回测时,确保所有数据都是“当时能获取到的”。比如用开盘价做信号,那就只能开盘后开仓;用收盘价做信号,那就只能第二天开盘再开仓。

13.5.2 幸存者偏差(Survivorship Bias)

幸存者偏差,是指只关注活下来的品种,忽略了已经死掉的

比如你回测期货策略,只用了当前还在交易的合约。但那些已经退市、或者被摘牌的合约呢?它们的表现可能很差。如果你把它们排除在外,回测结果就会虚高。

我记得有一次,我回测一个跨品种套利策略,结果特别好。后来才发现,我用的数据里只包含了“活到现在的品种”。而那些中途破产的品种,数据根本就没收录。这导致回测结果严重失真。

我的建议:尽量使用全样本数据,包括已经退市的品种。如果数据源不完整,至少要在报告中说明这一点。

13.5.3 过拟合(Overfitting)

过拟合,就是策略在历史数据上表现完美,但在未来数据上一塌糊涂。

怎么避免?

  • 参数不要太多。参数越多,越容易过拟合。
  • 使用样本外测试。把数据分成训练集和测试集,训练集上调参数,测试集上验证。
  • 做敏感性分析。稍微改变参数,看看策略表现是否稳定。

13.5.4 交易成本忽略

很多新手回测时,把交易成本设得很低,甚至设为0。这太天真了。

实际交易中,滑点、手续费、冲击成本,每一项都能吃掉你的利润。尤其是高频策略,交易成本的影响更大。

我一般会按实际成本的1.5倍来估算。这样即使实盘有偏差,也不至于太惨。

13.6 知识体系总览

下面这张图,帮你理清本章的核心逻辑:

交叉验证与回测知识体系 时间序列交叉验证 滚动窗口回测 扩展窗口回测 特点 • 保持时间顺序 • 无未来信息泄露 • 计算量较大 特点 • 窗口大小固定 • 适应市场变化 • 步长设置关键 特点 • 窗口只扩大 • 数据利用率高 • 受早期数据影响 常见陷阱 ⚠ 前视偏差 ⚠ 幸存者偏差 ⚠ 过拟合 ⚠ 交易成本忽略 ⚠ 未来函数 ⚠ 数据清洗不当

这张图把三种回测方法和常见陷阱都串起来了。你仔细看看,应该能对本章内容有个整体把握。

13.7 总结

回测不是万能的,但没有回测是万万不能的。关键是你要知道回测的局限性在哪里。

我个人习惯是:先用时间序列交叉验证做初步筛选,然后用滚动窗口做精细调参,最后用扩展窗口做稳定性测试。三个方法结合起来,能最大程度避免过拟合。

至于那些陷阱——前视偏差、幸存者偏差、交易成本——每次回测前,我都会逐条检查。宁可慢一点,也不能让策略带着bug上线。

一句话记住:回测的目的是发现错误,而不是证明正确。如果你跑完回测,发现全是优点,那大概率是你漏掉了什么。


公众号:蓝海资料掘金营,微信deep3321