跳到正文

金融中的交叉验证

随机 k 折在金融里因为三个独立原因失效。Walk-forward 修一个,净化(purging)修另一个,禁运(embargo)修第三个。CPCV 是唯一能给你路径分布的方法。

第 4 篇 · 共 12 篇 约 10 分钟

金融中的交叉验证:完整方案

交叉验证是量化机器学习里被过度自信使用最多的工具。每个量化研究员都跑过 k 折 CV,看了看平均分,就把结果叫“样本外”。他们大部分都错了。CV 分数不是样本外,它是用样本内记忆伪装成验证的度量。原因是金融数据有三个破坏独立同分布假设的性质,而标准 CV 恰好利用了这三个性质。

本文给出完整方案。我们会讲为什么标准 CV 会失败、为什么 walk-forward CV 是部分修正、为什么净化 CV 是进一步修正、为什么禁运在净化之上还是必要的,以及为什么组合净化交叉验证(CPCV)是唯一能给你有意义的路径分布的方法。

参考是 AFML 第 7 章和第 12 章。数学不深,靠的是纪律。

(López de Prado, M., Advances in Financial Machine Learning, 第 7、12 章。ISBN 978-1119482086。)

三种失效模式

标准 k 折 CV 假设观测独立同分布。金融观测不满足独立同分布,原因有三个独立的维度。

原因 1:时间泄露。

随机划分把时点 t1t_1 分到测试集,把 t2>t1t_2 > t_1 分到训练集。模型在 t2t_2 的数据上训练,却在 t1t_1 的数据上评估。模型已经看到了未来。它的“样本外”预测部分来源于本不该获得的训练数据。

在表格型机器学习里(图像分类、NLP),这没什么问题:行是独立同分布的样本,“未来”没有意义。在时间序列里,这是灾难性的。模型学到的是部署时无用的时间模式。

原因 2:自相关。

时点 tt 的收益和 t+1t+1 的收益相关。随机划分把大部分相邻样本放在同一个折(或者同时放在训练和测试里)。测试集和训练集没有本质区别:它只是时间上的邻居。

模型在本质上和训练集同分布的数据上评估。CV 分数变成模型插值能力的估计,而不是外推能力的估计。

原因 3:特征-标签重叠。

大多数特征基于滚动窗口计算。时点 tt 的特征依赖 t−wt-w 到 t−1t-1 的数据。如果标签是 tt 到 t+ht+h 的 hh 天前向收益,特征和标签在时点 tt 上重叠。

如果你的模型在时点 tt 上训练,它带有自己的特征和标签,而测试集包含时点 t+1t+1,带有它的特征和标签,那么 t+1t+1 的特征是用 t+1−wt+1-w 到 tt 的数据算出来的,其中包含 tt 的标签。模型在训练期间已经间接看到了测试标签。

这是最难发现的一种污染,因为代码看起来一切正常。特征算得对,标签算得对,划分是随机的,CV 分数高得可疑。

(AFML, §7.1–7.4。)

Walk-Forward CV:基线修正

第一个诚实的修正。不随机,沿着时间向前推进。

Fold 1: train [2000-01 to 2010-12],  test [2011-01 to 2011-12]
Fold 2: train [2000-01 to 2011-12],  test [2012-01 to 2012-12]
Fold 3: train [2000-01 to 2012-12],  test [2013-01 to 2013-12]
...
Fold k: train [2000-01 to 2019-12],  test [2020-01 to 2020-12]

训练集扩张(或者在滚动窗口变体中保持固定)。测试集总是严格在训练集之后。未来数据不会泄露到训练里。

两种变体:

扩张窗口:训练集不断增长。当你相信更多数据有用、且分布稳定时用这个。大部分量化策略用这个。

滚动窗口:训练集始终是最近 NN 个时期。当近期数据更相关时(分布漂移、状态切换)用这个。高频策略的典型选择。

Walk-forward 模拟实盘交易:在历史数据上训练,在未来数据上评估。CV 分数是对实盘表现远好于随机 CV 的估计。但它仍然偏乐观,原因有两个:

  1. 模型选择(超参数、特征、阈值)是通过看验证折决定的。验证折在严格意义上不是测试数据:研究人员已经用它做了决策。跨多个折的多重比较会放大乐观偏差。

  2. Walk-forward 没有考虑折之间的交易。在实盘交易中,你会交易这个模型,付出成本,然后把模型向前滚动。回测经常忘了建模这件事。

Walk-forward 夏普 1.0 大致相当于随机 CV 夏普 2.5–3.0。这就是典型的差距。

(AFML, §7.4。)

净化 CV:处理特征-标签重叠

Walk-forward 修正了时间划分。它没有修正特征-标签重叠。

重新说一遍问题:预测 r(t,t+h)r(t, t+h) 时,时点 tt 的特征依赖 t−wt-w 到 t−1t-1 的数据。标签窗口是 [t,t+h][t, t+h]。测试折覆盖 [Ttest,start,Ttest,end][T_{\text{test,start}}, T_{\text{test,end}}]。

训练样本 ttraint_{\text{train}} 的标签窗口是 [ttrain,ttrain+h][t_{\text{train}}, t_{\text{train}} + h],如果 [ttrain,ttrain+h]∩[Ttest,start,Ttest,end]≠∅[t_{\text{train}}, t_{\text{train}} + h] \cap [T_{\text{test,start}}, T_{\text{test,end}}] \neq \emptyset,它就和测试窗口重叠。如果是这样,这个训练样本的标签被测试期数据污染了。

修正是:当测试折结束于 Ttest,endT_{\text{test,end}},从训练集里删除所有标签窗口和测试窗口有重叠的样本。净化宽度至少是 hh(标签跨度),如果你还要删除那些特征依赖测试窗口数据的样本,最多可以是 h+wh + w。

规则:如果一个训练样本的标签窗口和测试窗口有重叠,就丢掉它。

实现:

def purged_train_times(times, test_times, embargo_pct=0.01):
    """Return times that are not in test, with embargo applied."""
    # Sort by time
    times = sorted(times)
    test_times = sorted(test_times)
    
    # Find embargo window after last test observation
    embargo_td = (test_times[-1] - test_times[0]) * embargo_pct
    
    # Drop observations in test
    train = [t for t in times if t < test_times[0]]
    
    # Apply embargo (drop observations within embargo of test end)
    train = [t for t in train if t <= test_times[-1] - embargo_td or t >= test_times[-1] + embargo_td]
    
    return train

净化后的 walk-forward CV 是时间序列量化机器学习的最低诚信标准。

(AFML, §7.4.2。)

禁运:另一个方向

净化处理“前”方向:标签用了测试期数据的训练样本。禁运处理“后”方向。

为什么“后”方向重要?考虑测试折结束于 Ttest,endT_{\text{test,end}}。下一个训练折(滚动 CV,或者测试后重拟合)紧接着开始。时点 Ttest,end+1T_{\text{test,end}} + 1 的特征依赖 Ttest,end+1−wT_{\text{test,end}} + 1 - w 到 Ttest,endT_{\text{test,end}} 的数据。时点 Ttest,endT_{\text{test,end}} 的特征依赖 Ttest,end−wT_{\text{test,end}} - w 到 Ttest,end−1T_{\text{test,end}} - 1 的数据,也就是在测试窗口里。

如果你的模型在测试折之后立即重训练,新的训练集里有些特征是用测试期数据算出来的。标签是干净的,但特征泄露了。

修正是:测试折在 Ttest,endT_{\text{test,end}} 结束后,跳过接下来 EE 天的训练数据。禁运宽度 EE 通常等于标签跨度 hh,和净化同样的逻辑,只是用在了另一侧。

净化加禁运一起给出净化后的 walk-forward CV,时间序列量化机器学习的最低诚信标准。

(AFML, §7.4.3。)

实务中的禁运

关于禁运有一个细节:它是不对称的。

测试折之前(净化):标签延伸到测试窗口的训练样本。删除它们。

测试折之后(禁运):特征是用测试期数据算出来的训练样本。删除它们。

“前”规则针对的是标签。“后”规则针对的是特征。它们覆盖了不同的泄露通道。

一个常见错误:在两边(测试折之前和之后)都施加禁运。“前”方向由净化处理,禁运在“前”方向上冗余或错误。禁运只应该施加在“后”方向。

另一个常见错误:把禁运设成固定的日历时间(例如 5 天),而不管特征窗口。禁运至少应该和最大的特征回看窗口一样宽。如果你的特征包括 60 天滚动均值,禁运至少要有 60 天。

(Hyndman, R. J., & Athanasopoulos, G., Forecasting: Principles and Practice, 3rd ed., §5.10。ISBN 978-0991577989。)

组合净化交叉验证(CPCV)

Walk-forward CV 给你一条穿过数据的路径:在 [2010, 2018] 上训练,在 [2019] 上测试;在 [2010, 2019] 上训练,在 [2020] 上测试;以此类推。结果是一串夏普比率。平均夏普是你对表现的单点估计。但没有不确定性量化。

CPCV 解决了这个问题。它通过同时抽取多个测试折来生成许多条穿过数据的路径。

算法:

  1. 把数据划分成 NN 个连续组(例如 N=6N = 6 对应 6 个连续年)。
  2. 对 NN 个组中每 kk 个子集(通常 k=2k = 2),指定这些组为测试集。
  3. 剩下的 N−kN - k 个组是训练集,在每个测试边界上施加净化和禁运。
  4. 在净化后的训练集上训练模型,在测试集上评估。
  5. 对全部 (Nk)\binom{N}{k} 个子集重复上述过程。

当 N=6N = 6、k=2k = 2 时,你会得到 (62)=15\binom{6}{2} = 15 条路径。每条路径产出一个夏普比率。你现在有 15 个夏普的分布,不是一个单一数字。

优点:

路径分布:不是单个夏普,而是 15 个。均值是估计,标准差是不确定性。

多个回测路径:每条路径都是一次完整的回测。15 条路径合起来能让你感受到策略在各个历史时期的表现。

没有泄露:每条路径都使用净化和禁运,所以没有时间泄露。

没有多重比较偏差:你没有挑出最好的一条路径,你用了所有路径。

缺点:

计算成本:15 条路径意味着 15 次模型拟合。对慢模型(例如神经网络),成本可能高得难以接受。

非独立性:15 条路径共享训练数据。夏普比率相关,这让不确定性量化变复杂。但相关结构本身是有信息量的。

(AFML, §12.3。)

选择 NN 和 kk

组数 NN 和测试集大小 kk 是超参数。常见选择:

  • N=6N = 6、k=2k = 2:15 条路径,每个测试集包含 2 个连续组。
  • N=10N = 10、k=2k = 2:45 条路径,粒度更细。
  • N=6N = 6、k=3k = 3:20 条路径,测试集更大。

NN 越大路径越多,但每个测试集更小(每条路径的统计功效低)。NN 越小路径越少,但每个测试集数据更多。

一个实务经验法则:NN 要让每组包含足够数据来训练一个稳定的模型(例如 1–2 年日频数据)。kk 要让每个测试集有实际意义(例如 6–12 个月)。

对大多数流水线,N=6N = 6、k=2k = 2 是一个合理的起点。

回测重叠问题

即使使用 CPCV,回测路径也会重叠。路径 1 可能用 [2010, 2011] 作为测试。路径 2 可能用 [2010, 2012] 作为测试。训练集大量重叠。夏普比率相关。

这没问题,只要理解相关性的含义。意思是路径不是独立的性能估计,它们是相关的估计。有效样本量小于 15。

一个粗略的修正:计算夏普比率的平均两两相关性,用 NN 除以 (1+(N−1)ρˉ)(1 + (N-1)\bar{\rho})。对 ρˉ=0.3\bar{\rho} = 0.3、N=15N = 15,有效样本量是 15/(1+14⋅0.3)≈3.115 / (1 + 14 \cdot 0.3) \approx 3.1。这比 15 小很多,不确定性量化会反映这一点。

(Bailey, D. H., & López de Prado, M., “The Deflated Sharpe Ratio,” Journal of Portfolio Management, 40(5), 2014。)

完整例子

设定:6 年日频数据(2019–2024),标签是 h=5h = 5 的三重障碍,特征包括 60 天动量和 20 天波动率。

CPCV 用 N=6N = 6 组(每年一组)和 k=2k = 2 个测试组(2 个连续年作为测试)。总路径数:(62)=15\binom{6}{2} = 15。

对每条路径,在净化后的训练集(4 年数据,测试窗口前沿 5 天净化,测试窗口后沿 60 天禁运)上拟合模型,在 2 年的测试集上评估。

15 条测试路径是:

  • [2019, 2020] 测试,在净化后的 [2021, 2024] 上训练
  • [2019, 2021] 测试,在净化后的 [2022, 2024] 上训练
  • [2019, 2022] 测试,在净化后的 [2023, 2024] 上训练
  • [2019, 2023] 测试,在净化后的 [2024] 上训练(训练集很小)
  • [2019, 2024] 测试,在净化后的 [] 上训练(无训练数据)
  • [2020, 2021] 测试,在净化后的 [2019, 2022–2024] 上训练
  • ……等等。

有些路径训练数据极少(例如在 [2019, 2024] 上测试就没有训练数据)。你可以选择丢掉这些路径,或者把它们当作低置信度估计。

结果:15 个夏普比率。假设均值是 1.2、标准差是 0.5。估计是“夏普在 1.2 附近,有相当大的不确定性”。

对比单个 walk-forward CV:你会有一个夏普,比如说 1.5。CPCV 告诉你这个 1.5 是众多可能值中的一个;更现实的估计接近 1.2,且置信区间更宽。

常见错误

反复出现的五种错误。

错误 1:跳过净化。

没有净化的 walk-forward 有特征-标签重叠。CV 分数向上偏。策略在实盘会表现更差。

错误 2:忘记禁运。

有净化但没禁运的 walk-forward,在后沿有特征泄露。CV 分数向上偏。策略在实盘会表现更差,特别是在状态过渡时。

错误 3:用随机 k 折。

随机 k 折同时具有三种失效模式。CV 分数没有信息量。策略在实盘会表现更差,通常差很多。

错误 4:净化过于激进。

如果你净化得超过必要的程度(例如在只需要 hh 的时候净化 2h2h),你就在丢失训练数据的同时,并没有进一步减少泄露。CV 分数会变得方差更大,但不会更诚实。

错误 5:用 CV 做超参数选择,但没有留出最终测试集。

如果你把整个数据集都用于 CV 超参数选择,胜出的超参数就是最贴合历史数据的那些,包括你想去掉的所有泄露模式。最终测试应该是一个从未用于任何决策的 holdout 集。

CV 能告诉你什么、不能告诉你什么

CV 能告诉你:

  • 模型在样本外数据上的预期表现,前提是数据生成过程平稳。
  • 这个表现在不同历史路径上的方差。
  • 在你的数据上,一个模型是否比另一个明显更好。

CV 没法告诉你:

  • 策略在未来会不会表现好(数据生成过程可能变化)。
  • 策略能不能扛住交易成本(CV 通常不建模成本)。
  • 策略能不能扛住容量限制(CV 假设你能按预测价格成交)。

诚实的说法是:CV 是模型选择工具,不是部署决策工具。一个赢得 CV 的模型只是部署的候选。是否部署取决于其他因素(容量、成本、对状态变化的稳健性),CV 不管这些。

(López de Prado, M., Advances in Financial Machine Learning, 第 7、12 章。ISBN 978-1119482086。)