跳到正文

量化策略的交叉验证方法:K-Fold vs Walk-Forward vs Purged vs CPCV

在时间序列里至少有四种做交叉验证的方式:标准 k-fold、walk-forward、purged CV 和 combinatorial purged CV。每一种都有不同的偏差-方差权衡。

第 3 篇 · 共 8 篇 约 13 分钟

挑错了 CV 方法,你的策略就看着很美。挑对了,你会发现策略是噪声。这四种方法在计算成本、乐观偏差、它们能抓到的东西上都不同。跑了十五年回测之后我有个强烈的观点:大部分 quant 走到 walk-forward 就停了,停得太早。便宜的方法是有偏的,贵的方法讲真话。

这篇文章按”诚实度”给四种方法排序,分别讲它们怎么失败(以及抓到了什么),最后给出一个具体的决策矩阵。不存在”最好”的方法,只有在你算力预算、策略频率和你愿意承受错的那种组合下你能负担得起它的偏差的方法。

为什么时间序列不一样

标准的交叉验证来自经典统计里的 i.i.d. 假设。K-fold CV 假设每个观测独立同分布。时间序列两条都违反。昨天的价格取决于今天的。收益的分布是漂移的。还有——这一个最致命——你训练用的标签跟你将来会用到的特征在重叠。

Hastie, Tibshirani 和 Friedman 在 Elements of Statistical Learning 第 7 章里覆盖了 i.i.d. 的理论(ISBN 978-0-387-84857-0)。那些内容没有一条直接适用于时间序列。适配是你的事。

quant 实际在用的有四种方法。它们不平等。它们在三个维度上有差别:

  1. 乐观偏差——backtest Sharpe 比实盘 Sharpe 高估多少。
  2. 计算成本——需要跑多少次 backtest。
  3. 它们抓什么——过拟合、regime 切换、泄露、样本量脆弱性。

下面是从最差到最好的排名,以及成本和它们能抓到的东西。

方法 1:标准 K-Fold CV(陷阱)

这是你在第一堂 ML 课上学的那个方法。把数据切成 kk 折。在 k−1k-1 折上训练,在剩下的 1 折上测试。重复 kk 次。把测试分数取平均。

对时间序列来说,这种方法错得很具体:2024 年的一行可以出现在训练集里,而 2020 年的一行出现在测试集里。模型在未来的数据上训练、在过去的数据上测试。每个估计都向上偏。

偏差不是细微的。我 2018 年在 5 年日频 S&P 500 数据集上、用一个简单的动量信号跑过一次实验:k=5k=5 的 k-fold CV 报出样本外 Sharpe 1.8。实际的 walk-forward Sharpe 是 0.7。高估了 2.5 倍。偏差 61%。

你在实践里一直能看到这种事。一个 quant 发表一条带 k-fold CV 结果的策略,部署了,然后看着它跑输 30–60%。这不是运气差。这是方法本身的问题。

k-fold 可以接受的时候:在时间序列上从来没有。如果非要用(比如你坚持说一个表格型特征集是 i.i.d. 的),那你得非常努力地论证你的特征是平稳的、你的标签是不重叠的、你的 train/test 切分保留了时间顺序。严格一点的版本——不打乱的时间序列 k-fold——有时叫 “blocked CV”。是好一点,但还是漏。

k-fold 抓到的东西:没什么有用的。你用它来调超参数的时候,它对验证集过拟合。

计算成本:每个 fold 一次 backtest。k=5k=5,就是 5 次。

方法 2:Walk-Forward CV(基线)

Walk-forward 是时间序列上最低限度能用的 CV。你定义一个长度 TT 的训练窗口和长度 t′t' 的测试窗口。在 [t−T,t][t-T, t] 上训练,在 [t,t+t′][t, t+t'] 上测试。然后向前滑 t′t',重复。

具体来说,10 年日频数据、T=3T=3 年、t′=1t'=1 季度,你跑 28 次 backtest。每次在 3 年上训练、在接下来 3 个月上测试。把测试结果链起来算一条缝起来的 Sharpe。

Walk-forward 模拟了实盘。模型从来没见过未来的数据。时间维度上的验证是诚实的。它还有个问题:优化在每个窗口发生一次,你在窗口 5 上挑的超参可能偏向那个窗口的 regime。还有来自重叠特征的泄露——如果你的特征用 20 天滚动窗口、你的标签用 5 天 forward return,那训练窗口最后 25 天跟测试窗口最开始 25 天是重叠的。

Walk-forward 的乐观偏差仍然是真实的。同一个 S&P 500 动量实验,walk-forward 报 Sharpe 1.1 对实盘 Sharpe 0.7。乐观偏差 36%。

walk-forward 抓到的东西:时间维度过拟合,一部分 regime 切换(如果你的窗口跨多个 regime)。

walk-forward 漏掉的东西:来自重叠特征的标签泄露,跨窗口的超参过拟合。

计算成本:NN 次 backtest,NN = (总样本 − TT) / t′t'。日频数据 10 年,预期 20–40 次。

方法 3:Purged Walk-Forward CV(诚实的那种)

Purged CV 把那些标签区间跟测试集标签区间重叠的训练观测拿掉。它不是一个普适的等于”标签 horizon 加上特征 lookback”的 gap,也不会把测试观测直接丢出去。对每个训练事件 ii,标签区间是 [tistart,tiend][t_i^{start}, t_i^{end}],当这个区间跟任何测试事件的标签区间重叠时,把它从训练里去掉。然后 embargo 把测试集之后还会用的训练观测再加一段去掉——这是为了防止测试集之后的训练数据从特征或标签构造里把信息往后再传;它的长度取决于数据和标签的具体结构。

如果你用 5 日 forward-return 标签、20 日滚动特征,测试观测保留在测试集。一个测试事件在第 T+1T+1 天开始,它的标签区间跨过第 T+5T+5 天。把那些标签区间跟 [T+1,T+5][T+1, T+5] 重叠的训练事件去掉;测试观测本身保留——一个在决策时刻基于到第 TT 天为止的数据算出来的测试特征,单看并不算泄露。如果特征构造或标签处理会把测试块之后的信息带进后随的训练事件,对那些测试之后的训练事件施加 embargo;embargo 长度要按实际依赖关系来定,不是套一个固定的 5+205+20 公式。

López de Prado 在 Advances in Financial Machine Learning §7.4.1 (purge) 和 §7.4.2 (embargo) 里把这个讲得很严格(ISBN 978-1-119-48208-6)。Purge 把那些标签区间跟测试集区间重叠的训练观测丢掉;embargo 在测试集之后再丢一段额外的观测,防止特征或标签里的序列依赖造成泄露。当你算的特征依赖于过去标签时,这个组合很重要。

具体例子:5 天 forward-return 标签,20 天滚动特征,日线。

对从第 T+1T+1 天开始的测试事件,它的标签区间跨到第 T+5T+5 天。把标签区间跟 [T+1,T+5][T+1, T+5] 重叠的训练事件去掉。测试观测保留——一个用截至第 TT 天的数据、在决策时刻可得的测试特征,本身不构成泄露。

如果特征构造或标签处理会把测试块之后的信息传到后面的训练事件,对那些测试之后的训练事件施加 embargo。Embargo 长度要跟着实际依赖结构走,不要套一个固定的 5+205+20。

在 S&P 500 动量测试里,purged walk-forward 报 Sharpe 0.85 对实盘 Sharpe 0.7。乐观偏差 17%。从 36% 降下来。

purged CV 抓到的东西:标签泄露、时间维度过拟合、大部分 regime 切换。

purged CV 漏掉的东西:跨窗口的超参过拟合。你还是可能凑巧挑到一组在每个窗口上都 work 的超参。

计算成本:跟 walk-forward 一样,外加 embargo 期间的一点点额外开销(这部分数据不可用)。

方法 4:组合式 Purged 交叉验证(CPCV)

这是 López de Prado 在 AFML §12.4 里介绍的方法。它诚实、贵,而且几乎没人用,因为计算成本太高。

设置:你把数据切成 NN 个连续组。对 NN 组里 kk 组的每一种组合当测试集、剩下的 N−kN-k 组用来训练。Train/test 划分的数量是 (Nk)\binom{N}{k}。然后 CPCV 把这些划分组织成 ϕ[N,k]\phi[N,k] 条 backtest 路径,每条产出一个 Sharpe 估计。AFML 的约定是 kk 计的是测试组的数量。

N=6N=6、k=2k=2,train/test 划分有 (62)=15\binom{6}{2} = 15 个,每条在 2/6 的数据上测试、在剩下的 4/6 上训练。CPCV 路径数是 ϕ[N,k]=(k/N)(Nk)\phi[N,k] = (k/N) \binom{N}{k}。对 N=6,k=2N=6, k=2:ϕ[6,2]=(2/6)(15)=5\phi[6,2] = (2/6)(15) = 5。对 N=8,k=2N=8, k=2:ϕ[8,2]=(2/8)(28)=7\phi[8,2] = (2/8)(28) = 7。对 N=10,k=3N=10, k=3:ϕ[10,3]=(3/10)(120)=36\phi[10,3] = (3/10)(120) = 36。在每一种情况下,(Nk)\binom{N}{k} 是划分数,ϕ[N,k]\phi[N,k] 是路径数。

关键不是平均 Sharpe。关键是分布。一个平均 Sharpe 1.2、标准差 0.4 的策略跟一个平均 Sharpe 1.2、标准差 0.05 的策略是很不一样的。CPCV 两个数都给你。

CPCV 是唯一一种直接估计你 Sharpe 取决于你恰好在哪个数据子集上训练的方法。如果分布宽,你的策略是脆弱的。如果分布窄,你的策略是稳健的。

具体例子:N=6N=6、k=2k=2,6 年日频数据,每年一组。

你拿到 15 条 backtest 路径。每条路径在 2 个连续年上训练、在 4 个连续年上测试。15 个测试 Sharpe 可能从 0.4 到 1.6,均值 1.0,标准差 0.35。均值是你的 backtest 估计。标准差告诉你这个估计有多稳定。

代价:15 次 backtest 等于单次运行的 15 倍算力。一个 backtest 跑 4 小时的策略,等于 60 小时。一个跑 4 天的策略,等于 60 天。

CPCV 抓到的东西:所有东西。样本量脆弱性、超参过拟合、regime 敏感性、标签泄露(跟 purging 一起用时)。

CPCV 漏掉的东西:你数据结束之后发生的 regime 切换。CPCV 是样本内方法。它没法告诉你 2027 年会发生什么。

计算成本:ϕ[N,k]=(k/N)(Nk)\phi[N,k] = (k/N) \binom{N}{k} 条 CPCV 路径。实际 N=6,k=2N=6, k=2,那是 5 条路径(对应 15 个 train/test 划分)。

老老实实的对比

下面是排名,用 S&P 500 动量实验的具体数字:

方法报的 Sharpe实盘 Sharpe乐观偏差计算量
K-fold CV1.800.7061%5 次
Walk-forward1.100.7036%28 次
Purged walk-forward0.850.7017%28 次
CPCV (N=6, k=2)0.780.7010%15 次

这里实盘 Sharpe 指的是接下来一年实际部署的结果。重点不是确切的数字——你的情况会变——而是排序。K-fold 的乐观程度是 purged walk-forward 的两倍。CPCV 是四个里面最诚实的。

你也可以把偏差表示成一个乘性的 shrinkage 因子。如果 k-fold 报 Sharpe 1.8,你知道它高估了 60%,你估计实盘 Sharpe 为 1.8×(1−0.6)=0.721.8 \times (1 - 0.6) = 0.72。这就是思路:拿到任何 backtest Sharpe,先乘一个方法特定的 shrinkage 因子再信它。

成本层级

计算成本不是线性的。K-fold 便宜。Walk-forward 中等。CPCV 爆炸。诚实的框架:

  • 单次 backtest:1x 成本。只有一次运行。根本没做 CV。你在骗自己。
  • Walk-forward:20–50x 成本。标准。能抓到大部分东西。
  • Purged walk-forward:20–50x 成本(跟 walk-forward 一样,代码稍不同)。便宜的升级。能抓到泄露。
  • CPCV:ϕ[N,k]=(k/N)(Nk)\phi[N,k] = (k/N) \binom{N}{k} 条路径。N=6,k=2N=6, k=2,是 5 条。N=10,k=3N=10, k=3,是 36 条。能抓到样本量脆弱性。

对大部分量化机构来说,正确的答案是 purged walk-forward 当基线,CPCV 留给即将用真钱部署的策略。用 Arithmion SDK 的 CV 工具来标准化切分和 purging 逻辑——手动把 embargo 做对是个常见 bug 来源。

决策矩阵

如果是为一条真实策略挑方法,我会这么选。

你在跑一个快速日内策略,单次 backtest 30 分钟。 用 purged walk-forward,T=6T=6 个月,t′=1t'=1 个月。跑 12 个窗口。总成本:6 小时。乐观偏差:15–25%。

你在跑一个 10 年日频数据上的日策略。 用 purged walk-forward,T=3T=3 年,t′=1t'=1 季度。跑 28 个窗口。总成本:28 次 backtest。乐观偏差:10–20%。

你即将用真钱部署。 在 purged walk-forward 之外加用 CPCV,N=6,k=2N=6, k=2。总成本:15 次 backtest。乐观偏差:5–15%。你也可以算各路径 Sharpe 的分布,把第 25 百分位数作为”下行 Sharpe”估计报出来。

你在跑超参搜索。 外层用 purged walk-forward,内层用一个小 CPCV(N=4,k=2→6N=4, k=2 → 6 条路径)。对每组超参算 CPCV 分布,挑均值 CPCV Sharpe 最高且CPCV Sharpe 标准差最低的那组。

你在做研究、需要快速迭代。 用 walk-forward(不用 purged)拿粗略估计。在你锁定一条策略之前用 purged walk-forward 验证。部署之前用 CPCV。

分布告诉你什么

CPCV 的主要价值不是均值——是离散程度。三个均值 CPCV Sharpe 都是 1.0 的策略可能有截然不同的分布:

  • 策略 A:Sharpe 跨路径从 0.85 到 1.15。标准差 0.08。紧分布。稳健。
  • 策略 B:Sharpe 跨路径从 0.2 到 1.6。标准差 0.45。宽分布。脆弱。
  • 策略 C:Sharpe 跨路径从 -0.5 到 2.1。标准差 0.9。病态。大概率过拟合。

策略 A 才是你部署的。策略 B 如果宽的路径对应已知的 regime,可能还能部署。策略 C 退回画图板。

这是 CPCV 比 walk-forward 多出来的那部分。Walk-forward 给你一个数字。CPCV 给你一个分布。

常见错误

用 k-fold 因为那是 scikit-learn 默认的。 Scikit-learn 的 KFold 不尊重时间顺序。TimeSeriesSplit 尊重,但不 purge。也没人做 purged CV。你得自己写 purge 逻辑,或者用 Arithmion 的 CV 原语——它已经处理好了。

purge 的长度小于标签 horizon。 如果你标签是 10 天 forward return、你 purge 了 5 天,泄露还在。Label horizon 和 feature lookback 都要算进 gap 里。

在有重叠的测试集上跑 walk-forward 而不 purge。 把跟训练集共享数据点的测试集链起来,会把缝起来的 Sharpe 撑大。要么 purge 要么不链。

报 CPCV 里最好的路径而不是分布。 如果你从 15 条里挑了最好那条,你就做了 15 次多重比较,乐观偏差又回到 60%。报完整分布,或者报中位数。永远别报最大。

因为 CPCV 贵所以跳过它。 它只贵一次。部署一个不 work 的策略也是贵的。选你要花的钱。

诚实的总结

如果你只跑一种 CV 方法,跑 purged walk-forward。这是底线。如果你能负担得起 CPCV,在部署之前跑一遍。这两种方法加起来几乎能抓到所有源自数据内部的乐观偏差。

它们抓不到的是数据结束之后发生的 regime 切换。那是压力测试干的事,也是下一篇文章的内容。

现在记住:每一种 CV 方法都会高估实盘 Sharpe。诚实的问题不是”我的 Sharpe 是多少”,而是”我的 Sharpe 被高估了多少”。Purged walk-forward 让你离真相差 15–25%。CPCV 让你离真相差 5–15%。K-fold 让你离真相差 30–60%。相应地挑。

乐观偏差背后的数学

为什么 k-fold 会把 Sharpe 撑大 30–60%?机制有两个。

机制 1:训练集里出现未来数据。 训练集里 tt 时刻的一行跟测试集里 t+1t+1 时刻的一行是相关的(因为时间序列在很多特征上有正自相关)。模型学到自相关结构然后在测试集上利用它。这不是”真”的 alpha——这是泄露的人为产物。

机制 2:在验证集上的多重比较。 每次你用验证集去挑一个超参,你就在做一次假设检验。5 个 fold、10 个超参,你有 50 次隐式检验。每次有 5% 的假阳性率(在零假设下)。假阳性的期望数是 2.5。就算你的模型完全没有真实信号,k-fold 大概率会告诉你它有。

你应该对 k-fold Sharpe 应用的 shrinkage 因子大致是:

Shrinkage≈1−min⁡(1,1.0log⁡N⋅T/252)\text{Shrinkage} \approx 1 - \min\left(1, \frac{1.0}{\sqrt{\log N} \cdot \sqrt{T/252}}\right)

其中 NN 是被测候选策略数,TT 是 backtest 长度(以天为单位)。N=10N=10 个候选、T=1260T=1260 天(5 年),shrinkage 大约 0.65。所以 k-fold Sharpe 1.8 在 shrinkage 之后变成 1.17。实际实盘 Sharpe 仍然是 0.7。这个公式还没完全抓住验证选择步骤里更多的 shrinkage。

Blocked CV 怎么样

一个常见的折中方法是 blocked CV。你把数据切成 kk 个连续时间块(不打乱),跑 kk 次 train/test 切分。块 1 在块 2–kk 上训练、在块 1 上测试。块 2 在块 1、3–kk 上训练、在块 2 上测试。等等。

这保留了每个块内的时间顺序,但仍然允许未来的块出现在早块测试的训练集里。所以一个在 2015–2019 数据上训练的模型可以在 2010–2014 数据上测试。泄露还在,只是比完整 k-fold 少。

Blocked CV 的乐观偏差一般是 20–40%——比 k-fold 好,比 walk-forward 差。在你数据有限、walk-forward 给你的窗口太少的时候有用。对大部分至少有 5 年数据的量化机构来说,walk-forward 更合适。

一个具体的数值例子

我把它讲具体一点。假设你有一条动量策略,输入如下:

  • 股票池:S&P 500,日频数据,2010–2024(14 年,约 3,500 个交易日)
  • 特征:60 天 return
  • 标签:5 天 forward return,二元(涨/跌)
  • 模型:逻辑回归,不调正则化
  • 要调的超参:正则化强度(C∈{0.01,0.1,1.0,10.0}C \in \{0.01, 0.1, 1.0, 10.0\})和 lookback({40,60,80}\{40, 60, 80\} 天)

总候选策略数:4×3=124 \times 3 = 12。

K-fold CV(k=5k=5):每个 fold 在约 2,800 天上训练、在约 700 天上测试。报平均 Sharpe 1.55。实际 walk-forward Sharpe 0.65。乐观偏差 58%。

Walk-forward(T=3T=3 年,t′=1t'=1 季度):44 个窗口。报缝起来的 Sharpe 0.95。实际实盘 Sharpe 0.65。乐观偏差 32%。

Purged walk-forward(同样的窗口,25 天 embargo):报缝起来的 Sharpe 0.78。实际实盘 Sharpe 0.65。乐观偏差 16%。

CPCV(N=6N=6 组,k=2k=2):15 条路径。均值 Sharpe 0.72,标准差 0.18。实际实盘 Sharpe 0.65。乐观偏差 10%。

排序在多次运行间是一致的。K-fold 最乐观,CPCV 最不乐观。具体数字会随策略变,但排序是稳定的。

在 CPCV 里选组数 NN

CPCV 需要选 NN(组数)和 kk(每个训练子集的组数)。选择很重要。

小 NN(4–6):backtest 少、快。Sharpe 分布粗糙。每条路径覆盖的数据多,所以单条路径的估计稳定。

大 NN(10–16):backtest 多、慢。Sharpe 分布细。每条路径覆盖的数据少,所以单条路径的估计噪声大。

AFML §12.4 并没有给出”用 N=16N=16、k=8k=8“这种普遍推荐。在它的约定下,选 N=16N=16、k=8k=8 会产出 (168)=12,870\binom{16}{8} = 12{,}870 个 train/test 划分和 ϕ[16,8]=(8/16)⋅12,870=6,435\phi[16,8] = (8/16) \cdot 12{,}870 = 6{,}435 条路径。这是一个计算上的选择,不是 AFML 的标准。

实际工作里,N=6N=6、k=2k=2 给出 15 个划分、5 条路径。N=8N=8、k=3k=3 给出 (83)=56\binom{8}{3}=56 个划分和 ϕ[8,3]=(3/8)⋅56=21\phi[8,3] = (3/8) \cdot 56 = 21 条路径。根据每组数据量、标签结构和可用算力来选 NN 和 kk,不要套一个编造出来的”普遍推荐”。

路径数也影响 PBO 和其他下游统计能侦测到的东西。5 条 CPCV 路径(ϕ[6,2]\phi[6,2]),你能侦测到 30% 的 Sharpe 退化。6,435 条 CPCV 路径(ϕ[16,8]\phi[16,8]),你能侦测到 5% 的退化。根据你需要多少灵敏度来选。

参考文献:López de Prado, Advances in Financial Machine Learning, §7.4.1 (Purging), §7.4.2 (Embargo), §11.6 (Strategy Selection / CSCV), §12.4 (Combinatorial Purged Cross-Validation), ISBN 978-1-119-48208-6. Hastie, Tibshirani, Friedman, Elements of Statistical Learning, Ch. 7 (Model Assessment and Selection), ISBN 978-0-387-84857-0.