时间序列交叉验证:Purged K-Fold 与 Embargo
金融数据存在序列相关性,标准 K-Fold 交叉验证会失效。Purged K-Fold CV 通过引入清仓期(embargo)修正这一问题——原理如下。
标准交叉验证假设样本之间相互独立。金融数据对这个假设嗤之以鼻——今天的收盘价与明天的相关,你用未来数据训练模型来预测过去,回测看起来会惊艳无比,直到碰上真金白银。
Purged K-Fold CV 解决了这个问题。它引入两种机制:清仓(purging)移除训练集与测试集边界附近的样本,防止信息泄露;embargo 在每个测试集之后增加一段空白期,应对 Alpha 信号的持续性。
核心问题
假设你有 1000 天的收益率数据,要验证一个交易策略。如果天真地切成 5 折——第 1 折用 1-800 天训练,801-850 天测试;第 2 折用 1-850 天训练,851-900 天测试——问题来了:
第一,前视偏差:第 850 天的训练数据可能包含了第 851 天才公开的信息(财报、宏观数据)。第二,训练-测试重叠:用 1-850 天训练的模型,已经见过第 800 天的数据,而测试集在第 801 天,邻近泄露。
标准 K-Fold 没有考虑金融时间序列的自相关结构。结果是 CV 估计系统性偏乐观——策略在回测里表现更好,但实盘不是。
Purged K-Fold:机制
清仓移除紧邻测试集前后的一段缓冲区的样本。如果测试集是第 801-850 天,就清除第 n_purge 天之前和之后的训练数据——这里的 n_purge 取决于信息完全吸收到价格里需要多长时间。
训练集:1-800 天
清仓区:801-(800+n_purge) ← 从训练集移除
测试集:(801+n_purge)-(850+n_purge)
Embargo 区:(851+n_purge)-(850+n_embargo) ← 双方都移除
下一折: embargo 之后开始
选择 purge 和 embargo 大小的数学依据是特征半衰期。如果 Alpha 信号衰减时间常数为 τ,保守估计需要 n_purge ≥ τ 和 n_embargo ≥ 2τ。
Marcos López de Prado 的《Advances in Financial Machine Learning》第 7 章推导了正式条件:清仓间隙应满足 |t_i - s_j| > h,其中 h 是随机过程的记忆参数。
为什么仅有 Purging 不够:Embargo
Purging 处理的是边界问题。但金融数据的自相关延伸到边界之外——如果苹果因为财报震惊在第 T 天跌了 20%,市场在 T+1、T+2、T+3 天仍在消化这个信息。
Embargo 在每个测试集之后增加一段空白期。模型不训练来自 embargo 期内的样本。这对以下策略尤为重要:
- 高频策略(刻度数据在毫秒级存在自相关)
- 事件驱动策略(财报、宏观发布)
- 慢速均线策略(均线交叉会在多天之间表现出相关性)
一个合理的默认值:embargo = 测试集长度的 2 倍。如果测试集是 50 天,在其后 embargo 100 天。
公式
每折测试样本数:
其中 T 是总样本数,k 是折数,n_embargo 是 embargo 大小。
第 i 折的有效训练集大小:
CV 估计的样本外表现:
其中 是第 i 折测试集的均值收益。
Python 实现
import numpy as np
import pandas as pd
from sklearn.base import clone
def purged_kfold(X, y, k=5, pct_embargo=0.01, n_purge=None):
"""
Purged K-fold cross-validation for financial time series.
Parameters
----------
X : array-like, shape (n_samples, n_features)
Feature matrix
y : array-like, shape (n_samples,)
Labels or returns
k : int
Number of folds
pct_embargo : float
Percentage of samples to embargo after each test set
n_purge : int, optional
Number of samples to purge before/after test set.
If None, defaults to sqrt(n_samples)
Returns
-------
train_indices, test_indices : list of tuples
"""
n_samples = len(X)
n_embargo = int(n_samples * pct_embargo)
if n_purge is None:
n_purge = int(np.sqrt(n_samples))
n_per_fold = (n_samples - n_embargo) // k
fold_indices = []
for i in range(k):
test_start = i * n_per_fold + n_purge
test_end = test_start + n_per_fold
train_end = test_start - n_purge
train_start = 0
embargo_end = min(test_end + n_embargo, n_samples)
train_idx = list(range(train_start, train_end))
test_idx = list(range(test_start, test_end))
fold_indices.append((train_idx, test_idx))
return fold_indices
如何解读结果
Purged K-fold CV 给出了无偏估计——但前提是你正确解读它:
样本外均值收益告诉你策略在各折的平均表现。如果是负数,停止。如果是正数但波动大,策略存在结构性风险。
样本外收益的 Sharpe 比率(而非均值收益本身)才是正确的指标。日均收益 0.1%、标准差 0.5% 的策略, annualized Sharpe 约为 0.14——平庸,不是亮眼。
逐折明细比平均值更重要。如果第 1 折收益 2.0%、第 2 折 -1.8%,你的策略存在结构性风险,是均值掩盖了这种风险。报告最小和最大折收益。
常见陷阱
使用 pct_embargo=0:最常见的错误。如果不 embargo,就不是时间序列 CV。即使 1% 也比 0 好。
清仓过小:n_purge = sqrt(n) 是经验法则。如果你的信号衰减慢(例如 MA(200)),用更大的清仓。代价是效率——你会损失训练样本——但总比有偏估计好。
CV Sharpe 与实盘策略不一致:如果你的实盘策略每日调仓但 CV 用月度数据训练,CV 测量的根本不是正确的东西。CV 方法论必须与交易方法论匹配。
忽略 embargo 间隙导致回测过于乐观:如果你在每个测试折后 embargo 100 天,你的 Walk-forward 回测也应在样本内优化窗口和样本外测试之间有间隙。大多数从业者跳过这一步,得到的是乐观偏差。
折数过少:k=5 对 1000+ 样本是标准。如果在 500 样本上用 k=10,每折测试集只有 50 样本——太嘈杂,没有意义。
在知识体系中的位置
Purged K-Fold 是本节一切的基础。组合 Purged CV 将其扩展到训练-测试分割的组合枚举。Deflated Sharpe Ratio 以此处的样本外收益作为输入。PBO(回测过拟合概率)使用 CPCV(组合清仓 CV)作为核心方法论。
如果你要验证任何基于历史数据训练来预测未来收益的策略,你需要 Purged K-Fold 或其变体。没有捷径——标准 K-fold 会对你撒谎。
我们的平台在将任何策略纳入投资组合之前,会以 purged K-fold 作为基线检查。Purge 和 embargo 参数根据策略调仓频率进行校准。
参考文献
- López de Prado, Advances in Financial Machine Learning, 第 7 章. Wiley, 2018. ISBN: 978-1-119-48208-6.
- Hastie, Tibshirani, Friedman, The Elements of Statistical Learning, 第 7 章. Springer, 2009. ISBN: 978-0-387-84857-0.
- Bailey, Borwein, López de Prado, “Stock Return Data: A Non-Factorial Analysis,” Journal of Computational Finance, 2017.