跳到正文

禁运效应

净化处理前缘,禁运处理后缘。两者合在一起构成对特征-标签重叠的完整防御。跳过禁运,CV 仍然在撒谎。

第 5 篇 · 共 12 篇 约 10 分钟

禁运效应:单靠净化不够

净化删除标签延伸到测试折的训练观测。它处理前缘泄露:因为标签窗口前向延伸到测试期而污染的训练样本。净化讲清楚了,大部分实现都做。

禁运是另一半。它处理后缘泄露:测试折之后的训练观测,特征向后延伸到测试期。没有禁运,在测试之后的数据上训练出来的模型,所用的特征是用测试期数据算出来的。标签是干净的,特征不是。CV 分数向上偏,策略在实盘会表现更差。

本文讨论为什么禁运是必要的,宽度怎么设,它和净化怎么交互。数学和净化的推导(上一篇文章)一样,但纪律不同:大部分从业者跳过禁运,因为泄露是隐形的。

(López de Prado, M., Advances in Financial Machine Learning, §7.4.3。ISBN 978-1119482086。)

后缘泄露

想象时间轴。测试折从 TsT_s 到 TeT_e。训练数据既存在于 TsT_s 之前,也存在于 TeT_e 之后。

考虑时点 t>Tet > T_e 的训练观测。它的特征是用 [t−w,t−1][t-w, t-1] 的数据算的。如果 t−w<Tet - w < T_e,特征窗口就向后延伸到测试折。特征被污染了。

举个例子,假设特征是 20 天移动平均。在 t=Te+5t = T_e + 5 时,特征窗口是 [Te−15,Te+4][T_e - 15, T_e + 4]。这个窗口包括 TeT_e 到 Te+4T_e + 4(测试折之后)和 Te−15T_e - 15 到 Te−1T_e - 1(测试折开始之前和当时)。等等,TeT_e 是测试折的结束,所以窗口 [Te−15,Te+4][T_e - 15, T_e + 4] 包含 [Te−15,Te][T_e - 15, T_e],这部分和测试折 [Ts,Te][T_s, T_e] 重叠。

从 Te−15T_e - 15 到 Te−1T_e - 1 的 5 天在测试折内(假设 Ts≤Te−15T_s \leq T_e - 15,也就是测试折至少有 16 天)。t=Te+5t = T_e + 5 时的特征被污染了。

t=Te+5t = T_e + 5 时的标签是干净的(它的标签窗口是 [Te+5,Te+5+h][T_e + 5, T_e + 5 + h],不与测试折重叠)。只有特征泄露。

这就是后缘泄露。净化抓不到(净化只处理前缘的标签泄露)。禁运能。

禁运规则

删除下面区间内的训练观测:

[Te+1,Te+w][T_e + 1, T_e + w]

其中 ww 是数据集中最大的特征回看。禁运宽度是 ww。

有一个细节:禁运应该施加在“特征窗口会向后延伸到测试折”的观测上。严格规则是:如果 t−w≤Tet - w \leq T_e 就删除 tt。对训练观测 t>Tet > T_e,这等价于 t≤Te+wt \leq T_e + w。所以禁运区间是 [Te+1,Te+w][T_e + 1, T_e + w]。

宽度恰好是 ww,等于特征回看。

(AFML, §7.4.3。)

具体例子

设定:60 天动量特征,5 天持仓期,日频观测。测试折是 252 天(一个交易日年)。

时点 tt 的标签依赖 [t,t+5][t, t+5] 的数据。时点 tt 的特征依赖 [t−60,t−1][t-60, t-1] 的数据。测试折是 [Ts,Te][T_s, T_e],Te−Ts=252T_e - T_s = 252。

前缘:[Ts−5,Ts][T_s - 5, T_s] 内的训练观测,标签窗口延伸到测试折。这些要被净化。净化宽度是 5。

后缘:[Te+1,Te+60][T_e + 1, T_e + 60] 内的训练观测,特征窗口向后延伸到测试折。这些要被禁运。禁运宽度是 60。

总共“损失”的训练观测:5+60=655 + 60 = 65。对一个 252 天的测试折,这是测试折大小的 26%。训练集减少了 65 天。

对 10 年数据集(约 2,520 个交易日),损失是 65/2,520 = 2.6%。不大。但它阻止的泄露是有意义的:没有禁运,测试之后的训练观测的特征会被测试期数据污染,CV 分数向上偏,偏的幅度未知。

禁运最关键的时候

三种场景。

场景 1:长特征窗口。

如果你的特征包括 250 天移动平均,禁运就是 250 天。这在典型 252 天测试折里占相当大比例。禁运在后缘会让训练数据减少大约 50%。

场景 2:状态过渡。

如果测试折包含状态切换(例如金融危机),测试之后的数据有用了危机期数据算出来的特征。在这个数据上训练的模型被危机模式污染了。禁运能防止这一点。

场景 3:重拟合场景。

如果测试折结束后你重拟合模型,新的训练集包括禁运区。没有禁运,新模型的特征会“记住”测试折。禁运防止模型隐式记忆测试期。

三种情况下,禁运对诚实的 CV 都是必要的。

禁运不那么重要的时候

三种场景中,禁运的影响小一些。

场景 1:walk-forward CV,测试折内不重训练。

如果你在 [Ts−h−训练集大小,Ts−h−1][T_s - h - \text{训练集大小}, T_s - h - 1] 上训练一次,在 [Ts,Te][T_s, T_e] 上评估,那么测试之后没有训练数据。禁运不适用。

这是典型的 walk-forward 设置。禁运主要对滚动 CV 和重拟合场景重要。

场景 2:短特征窗口。

如果最大特征窗口只有 5 天,禁运是 5 天。数据损失极小。

场景 3:长测试折。

如果测试折是 1000 天,60 天的禁运是测试折大小的 6%。相对影响小。

与重拟合的交互

禁运最常见的应用场景:重拟合。

假设你在数据 [Ts−1][T_s - 1] 结束处训练,在 [Ts,Te][T_s, T_e] 上评估,然后在数据 [Te+禁运][T_e + \text{禁运}] 结束处重拟合。重拟合训练集在 [Te+1,Te+禁运][T_e + 1, T_e + \text{禁运}] 有数据,这些数据的特征向后延伸到 [Ts,Te][T_s, T_e]。重拟合的模型被污染了。

修正是:在重拟合训练集里跳过禁运区。重拟合模型在 [Ts−h−训练集大小,Ts−h−1]∪[Te+w+1,Te+新训练结束][T_s - h - \text{训练集大小}, T_s - h - 1] \cup [T_e + w + 1, T_e + \text{新训练结束}] 上训练,禁运区被排除。

代码:

def embargo_post_test(train_times, test_end, w):
    """Drop training observations in [test_end + 1, test_end + w]."""
    embargo_start = test_end + 1
    embargo_end = test_end + w
    return train_times[(train_times < embargo_start) | (train_times > embargo_end)]

一个常见错误:在多折 CV 方案的每一个测试折之后忘了施加禁运。如果你有 5 个测试折,就有 5 个禁运区。每个都必须从对应的训练集中排除。

净化和禁运的不对称

这两种机制形式类似,但精神不同。

净化:处理标签泄露。时点 tt 的标签依赖 [t,t+h][t, t+h] 的数据。如果这个窗口和测试折相交,标签被污染。污染的是训练观测的结果。

禁运:处理特征泄露。时点 tt 的特征依赖 [t−w,t−1][t-w, t-1] 的数据。如果这个窗口和测试折相交,特征被污染。污染的是训练观测的输入。

两者都是真实的,都让 CV 分数向上偏,都必须处理。

不对称在于方向。净化删除测试之前的训练数据。禁运删除测试之后的训练数据。它们互为镜像,但数学不同,因为标签窗口向前延伸(到 [t,t+h][t, t+h]),特征窗口向后延伸(到 [t−w,t−1][t-w, t-1])。

禁运宽度:应该多大?

严格规则:禁运宽度至少要等于数据集中最大的特征回看 wmax⁡w_{\max}。

一个有用的诊断:列出数据集中所有特征以及它们的回看窗口。最大值就是 wmax⁡w_{\max}。把禁运设为 wmax⁡w_{\max}。

更精确的规则:按特征设定禁运。对每个特征,检查它的窗口是否向后延伸到测试折。如果是,删除观测。计算量更大但更精确。

高频策略的一个经验法则:用测试折的某个百分比。252 天的测试折,60 天的禁运是 24%。21 天的测试折,5 天的禁运也是 24%。百分比方法会按测试折大小缩放。

(AFML, §7.4.3。)

百分比启发法

更简单的规则:把禁运设为测试折大小的某个百分比,比如 1% 或 5%。这是会随测试折大小缩放的经验法则。

252 天的测试折用 1% 的禁运,禁运是 2.52 天,取整为 3 天。这比典型 60 天动量特征窗口要窄。启发法对很多场景都太短。

252 天的测试折用 5% 的禁运,禁运是 12.6 天,取整为 13 天。还是比 60 天短。

百分比启发法对短特征窗口的高频策略有效。对长窗口的波段或仓位策略会失效。

更安全的规则:取(测试折的百分比,wmax⁡w_{\max})两者的最大值。

禁运与并发

进一步修正:当很多观测有重叠标签窗口时,测试之后训练数据的独特信息量很小。禁运应该考虑这一点。

和净化(上一篇文章)中一样的并发调整:

调整后的禁运宽度=c⋅w\text{调整后的禁运宽度} = c \cdot w

其中 cc 是标签的平均并发数。对 h=5h = 5 的日频三重障碍标签,cc 大约是 5。对 w=60w = 60,调整后禁运是 300 天。这比严格 ww 更激进。

理由是:如果每条测试后观测的标签和 cc 个其他标签重叠,跨 cc 个标签的复合特征泄露大于简单 ww。调整后的禁运考虑到了这一点。

(AFML, §4.4 和 §7.4.3。)

禁运与 CPCV

在 CPCV(组合净化交叉验证)中,每条穿过数据的路径有多个测试折。禁运必须施加在每个测试折的边界上。

一个有用的心智模型:对每个测试折 [Ts(k),Te(k)][T_s^{(k)}, T_e^{(k)}],在 [Ts(k)−h,Ts(k)][T_s^{(k)} - h, T_s^{(k)}] 上净化,在 [Te(k)+1,Te(k)+w][T_e^{(k)} + 1, T_e^{(k)} + w] 上禁运。再把所有测试折的禁运区取并集。剩下的时段就是训练集。

6 组、每条路径 2 测试组的 CPCV,有 15 条路径,每条有 2 个测试折。所有禁运区的并集可能覆盖了相当大一部分数据。

实务中要注意:某些观测可能被多个测试折禁运。这没问题,它们就是不在训练集里。训练集更小,但更干净。

(AFML, §12.3.2。)

常见错误:在两边都禁运

一个常见错误:在测试折两边(之前和之后)都施加禁运。这不对。

“之前”一侧由净化处理,不是禁运。净化处理标签泄露(标签延伸到测试折的训练观测)。在“之前”侧用禁运会用不同的机制处理同样的泄露,但是冗余的。

禁运专门针对测试之后的数据:特征向后延伸到测试折的训练观测。在“之前”侧施加禁运没有意义。

如果你的实现在两边都禁运,你是在过度激进:删除了本该保留的观测。

常见错误:重拟合时忘了禁运

常见错误:在测试折结束后重拟合时,施加了净化但忘了禁运。

想法是:“测试折已经结束了,禁运不相关。”但禁运针对的是测试之后的训练数据,下一次重拟合就会用到。如果重拟合时跳过禁运,新模型的特征就被测试期数据污染了。

修正是:在重拟合场景中始终施加禁运。禁运区与初始训练时一样:[Te+1,Te+w][T_e + 1, T_e + w]。

禁运与在线学习

在线学习中(后续文章会讲),模型会随新数据到来不断更新。禁运自动得到处理:测试之后的数据在特征不再向后延伸到测试期之前,都不会用于训练。

实际中,在线学习系统维护一个训练数据的滑动窗口。窗口至少要有 w+hw + h 宽,才能保证没有时间泄露。“禁运”隐含在窗口结构里。

一个有用的诊断

分别计算带禁运和不带禁运的 CV 分数。差异就是泄露偏差的估计。

如果差异小(例如有禁运夏普 1.5,没禁运夏普 1.55),泄露就小。策略可能比较稳健。

如果差异大(例如有禁运夏普 1.0,没禁运夏普 2.0),泄露就很大。策略的“真实”CV 分数远低于朴素 CV。没有禁运,你会部署一个实盘表现少 50% 甚至更多的策略。

在你用的每个 CV 方案上都跑这个诊断。如果差距大,就修禁运。如果差距小,这个数据集的禁运就不那么要紧。

禁运在 Walk-Forward 与滚动 CV 中

禁运的重要性取决于 CV 方案。

Walk-forward CV(扩张或滚动训练窗口,单向前测):测试折在样本末尾。测试之后没有训练数据。禁运不适用。

带周期性重拟合的滚动 CV:模型在每一步重拟合,使用直到当前日期的数据。测试之后的训练数据(之前测试折之后)用来拟合下一次迭代。禁运适用:跳过特征会向后延伸到之前测试折的后测区。

组合净化 CV(CPCV):多个重叠的测试折,每个都需要自己的禁运。所有禁运区的并集可能覆盖相当一部分数据。分别对每个测试折算禁运,然后取并集。

一个常见实现模式:

def cv_with_embargo(data, test_folds, h, w):
    """Apply purge and embargo for each test fold."""
    embargoed_indices = set()
    for test_start, test_end in test_folds:
        # Purge: leading-edge training observations
        purge_start = test_start - h
        purge_end = test_start
        embargoed_indices.update(range(purge_start, purge_end))
        
        # Embargo: trailing-edge training observations
        embargo_start = test_end + 1
        embargo_end = test_end + w
        embargoed_indices.update(range(embargo_start, embargo_end))
    
    # Mask out embargoed observations
    mask = np.ones(len(data), dtype=bool)
    mask[list(embargoed_indices)] = False
    return data[mask]

这个函数返回禁运索引被屏蔽后的数据。

泄露预算

一个有用的诊断:估算没有禁运时会发生多少泄露。

252 天的测试折,h=5h = 5、w=60w = 60:

  • 净化宽度:5 天。
  • 禁运宽度:60 天。
  • 总共“损失”的观测:65。

没有禁运时,60 个测试后观测的特征会用到测试期数据。泄露的大小取决于污染特征和测试折结果之间的相关性。

实际中,对稳定的特征窗口(例如 20 天动量),泄露很小;对长窗口(例如 250 天移动平均),泄露很大。对 250 天移动平均,250 天的禁运能防止大量泄露。

一个粗略的经验法则:没有禁运时的泄露与特征窗口大小 ww 成正比。ww 越大,泄露越大,禁运越重要。

(AFML, §7.4.3。)

禁运与样本权重

禁运删除训练观测。样本权重(来自样本权重一文)对剩下的观测降权。它们是互补操作。

典型的流水线:

  1. 施加禁运删除泄露观测。
  2. 施加唯一性权重处理重叠标签。
  3. 施加时间衰减处理状态漂移。
  4. 施加波动率缩放处理绝对收益异质性。

顺序很重要:先禁运(数据最干净),再唯一性(处理标签重叠),再时间衰减和波动率缩放(处理状态和绝对收益问题)。

禁运后训练集比原始小。加权后训练集大小不变,但观测权重不同。两个都是金融数据诚实机器学习所必需的。

(AFML, 第 4 章和第 7 章。)

禁运给你带来什么

一个诚实的 CV 分数。没有禁运,CV 分数会因特征泄露向上偏。有了禁运,分数更接近实盘表现。

改进通常让夏普降低 5%–20%(无偏夏普低于有偏夏普)。一个没有禁运看起来夏普 2.0 的策略,加了禁运后可能显示夏普 1.7。2.0 是乐观的,1.7 更接近现实。

代价是数据:禁运会从训练集中删除观测。对大多数流水线,代价不大(几个百分点的数据)。对特征窗口特别长(例如 250 天)的流水线,代价更大(最多可达测试折后缘大小的 50%)。

权衡是:更诚实的 CV(有禁运)vs 更多的数据(无禁运)。对大多数流水线,诚实的 CV 值得数据代价。第一次发现加了禁运的 CV 不同意未加禁运的 CV,你会庆幸当初做了禁运。

(López de Prado, M., Advances in Financial Machine Learning, §7.4.3。ISBN 978-1119482086。)