跳到正文

三重障碍方法

固定时间窗口标签用起来方便,但几乎总是错的。三重障碍方法按“实际先发生什么”来打标签:止盈、止损、还是超时。标签匹配策略实际交易的方式。

第 7 篇 · 共 12 篇 约 10 分钟

三重障碍方法:标签与你的交易方式对齐

标签是一个问题。问题是:“这笔交易是赚是赔?”标签的选择决定了模型学到什么。大部分量化机器学习流水线用固定时间窗口的标签,比如“未来 5 天价格是否上涨?”这是文献中最常见的标签,但也是大多数交易策略的错配标签。

错配标签的问题很隐蔽。交叉验证看起来正常,回测夏普 1.8,模型上线后夏普掉到 0.4。你盯着实盘交易发现,策略 70% 的情况下是在止损或止盈阈值上平仓的,而不是在 5 天标记上平仓。你的标签是 5 天前向收益,但你的平仓是触发了障碍。模型学的是预测 5 天收益,策略交易的是障碍触发。这是两个不同的问题。

三重障碍方法正是为了修正这一点。它用实际先发生的事来标记每条观测:止盈障碍、止损障碍、还是纵向(时间)障碍。标签和策略的平仓逻辑一致。

参考是 AFML 第 3 章。这一章不长,实现也直接,收益却很大。

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

三个障碍

对每个入场时点 t0t_0,定义三个障碍。

上障碍(止盈):t0+Δtt_0 + \Delta_t,其中 Δt\Delta_t 由价格首次触及 pt0⋅(1+πup)p_{t_0} \cdot (1 + \pi_{\text{up}}) 的时刻决定。当价格先触及这个水平时,标签是 +1+1(或你用来表示“赢”的值)。

下障碍(止损):t0+Δtt_0 + \Delta_t,其中 Δt\Delta_t 由价格首次触及 pt0⋅(1−πdown)p_{t_0} \cdot (1 - \pi_{\text{down}}) 的时刻决定。当价格先触及这个水平时,标签是 −1-1(或“亏”)。

纵向障碍(超时):t0+ht_0 + h,其中 hh 是最大持仓期。当到 hh 时两个障碍都没被触发,标签由收益 r(t0,t0+h)r(t_0, t_0 + h) 的符号决定。这就是“都没触发”的情况。

观测 ii 的标签是:

yi=sign(min⁡(pt0+h,min⁡s∈(t0,h]ps⋅1[ps≤pt0(1−πdown)],max⁡s∈(t0,h]ps⋅1[ps≥pt0(1+πup)])−pt0)y_i = \text{sign}\left(\min\left( p_{t_0+h}, \min_{s \in (t_0, h]} p_s \cdot \mathbb{1}[p_s \leq p_{t_0} (1 - \pi_{\text{down}})], \max_{s \in (t_0, h]} p_s \cdot \mathbb{1}[p_s \geq p_{t_0} (1 + \pi_{\text{up}})] \right) - p_{t_0}\right)

换句话说,标签是“哪个障碍先被触发,那个触发点的价格相对入场价格的变化”的符号,上限是纵向障碍。

有一个细节:障碍可以是动态的(基于滚动波动率),也可以是静态的(固定百分比)。动态障碍会随近期波动率缩放:平静状态时障碍更紧,波动状态时更宽。这样在各个状态下标签分布更一致。静态障碍更简单,但标签分布会随波动率状态变化(波动期更多 −1-1,平静期更多超时)。

动态形式:

πup=πdown=k⋅σt\pi_{\text{up}} = \pi_{\text{down}} = k \cdot \sigma_t

其中 σt\sigma_t 是时点 tt 的滚动实现波动率,kk 是一个常数(通常 1.0 到 2.0)。同一个 kk 用在两边,让障碍在波动率单位上对称。

为什么固定时间窗口标签会失败

四个理由,每一个都够用。

理由 1:它忽略了策略的实际平仓逻辑。

带固定时间窗口标签的回测假设你无论发生什么都会持有 hh 天。如果你的实盘策略是先在止损或止盈阈值上平仓,回测就是在撒谎。固定时间窗口收益是“如果持有到期”的收益。实际策略收益是“障碍触发时的收益”。在波动状态中两者可能差非常多。

理由 2:它给出的标签分布不真实。

假设你的标签是 5 天前向收益。标签大致在 0 周围正态分布,不会偏向 ±π\pm \pi。障碍标签则不同:上下障碍距离入场是固定距离,所以当纵向障碍触发时,价格的移动小于障碍允许的幅度。标签更双峰化:大量 −1-1,大量 +1+1,中间夹着少量小收益。

这在分类任务中很重要。障碍标签训练出的模型学的是预测“触发了哪个障碍”,这是离散事件。固定时间窗口收益训练出的模型学的是预测连续变量,是不同的问题。

理由 3:它不感知路径。

两条路径可以产生同样的 5 天收益:一条平滑上涨,或者一次急跌后反弹。固定时间窗口标签对两者一视同仁。三重障碍标签能区分它们:平滑上涨可能在第 2 天就触发上障碍(标签 +1+1,收益被截断),先跌后涨的路径则在纵向障碍触发时只剩小正收益(标签取决于符号)。模型从两种路径中学到的是不同的东西。

理由 4:它表达不了风险管理约束。

大多数交易策略都有最大亏损阈值。如果价格朝你反向移动了 X%X\%,就平仓。固定时间窗口标签没有编码这个信息。带止损的策略会在 5 天标记之前平仓,所以 5 天收益根本没有被实现。三重障碍标签直接编码了平仓逻辑。

什么时候固定时间窗口标签也可以

三种情况下简单方法也够用。

情况 1:买入并持有策略。 如果你买入后从不平仓(或只在某个固定未来日期平仓),固定时间窗口标签是对的。没有障碍。

情况 2:横截面研究。 如果你研究的是“5 天收益的预测因子”,把它当学术问题而不是建交易策略,固定时间窗口标签是合适的。

情况 3:连续仓位管理。 如果你的策略根据预测收益连续调整仓位(而不是简单的多/空二元决策),固定时间窗口标签比障碍标签给出更平滑的训练目标。

大多数其他情况下(大多数带止损或止盈的交易策略),三重障碍是更合适的选择。

Side 与 Size:一种有用的区分

AFML 给出了一个有用的分解:标签可以分成两部分。

Side:交易方向。这是多头、空头还是无仓位?由哪个障碍先触发(或超时收益的符号)决定。

Size:仓位大小。这笔交易应该 100% 满仓、50% 半仓还是 0% 空仓?由预测的置信度决定。

主模型预测 side(方向)。次级模型(meta-labeling 模型)预测主模型是否正确。次级模型的输出成为 size。

这个分解是 meta-labeling 的基础,下一篇文章会详细讲。现在要明白的是:三重障碍标签为这两个模型都提供了天然的训练目标。

(AFML, §3.5。)

实现

一个干净的 Python 实现:

import numpy as np
import pandas as pd

def triple_barrier_labels(prices, events, pt_sl, target, min_ret=0.0):
    """Apply triple-barrier labeling.
    
    prices: DataFrame of prices indexed by time.
    events: DataFrame with columns [t1] (vertical barrier) and [trgt] (target volatility).
    pt_sl: list of [profit_taking, stop_loss] multipliers on target.
    target: pre-computed daily volatility series.
    """
    events = events.copy()
    events['t1'] = events['t1'].fillna(prices.index[-1])
    
    # Upper and lower barriers based on target volatility
    events['trgt'] = target.loc[events.index]
    events['sl'] = events['trgt'] * pt_sl[0]   # stop-loss width
    events['pt'] = events['trgt'] * pt_sl[1]   # take-profit width
    
    # For each event, find when each barrier is hit
    labels = []
    for t0, t1 in events[['t1']].itertuples():
        path = prices.loc[t0:t1]
        # Compute path returns from entry
        rets = path['close'] / path['close'].iloc[0] - 1
        # Find first time upper barrier is hit
        pt_hit = rets[rets >= events.loc[t0, 'pt']].index
        pt_hit = pt_hit[0] if len(pt_hit) > 0 else pd.NaT
        # Find first time lower barrier is hit
        sl_hit = rets[rets <= -events.loc[t0, 'sl']].index
        sl_hit = sl_hit[0] if len(sl_hit) > 0 else pd.NaT
        # Determine which was first
        first_hit = min(pt_hit, sl_hit)
        # If neither, label by sign of return at t1
        if pd.isna(first_hit):
            label = np.sign(rets.iloc[-1]) if abs(rets.iloc[-1]) > min_ret else 0
        else:
            label = 1 if first_hit == pt_hit else -1
        labels.append((t0, label))
    
    return pd.DataFrame(labels, columns=['t0', 'label']).set_index('t0')

函数对每个事件时间返回 −1-1、00 或 +1+1。00 对应“障碍都没触发,且收益小于 min_ret”。通常你会删除这些样本,或者按策略需要把它们当成 −1-1(不交易)。

障碍宽度的选择

两个参数:止盈倍数 kupk_{\text{up}} 和止损倍数 kdownk_{\text{down}}。通常取一样的值,但也可以不一样。

对称障碍:kup=kdown=kk_{\text{up}} = k_{\text{down}} = k。没有不对称先验时使用。最常见的起点。

不对称障碍:kup>kdownk_{\text{up}} > k_{\text{down}}(或反之)。策略有方向性偏好时使用。纯多头策略可能用 kup=2,kdown=1k_{\text{up}} = 2, k_{\text{down}} = 1,愿意在上涨方向上承受两倍的目标,但下跌方向快速止损。这会让标签分布偏向策略天然的方向。

合理的起点:k=1.0k = 1.0(障碍距离入场 1 个日波动率)。日波动率 1.5% 时,障碍是 ±1.5%\pm 1.5\%。5 天最大持仓的纵向障碍给 5 天时间去触发任一障碍。

怎么选 kk:用和超参数一样的交叉验证逻辑。尝试 k∈{0.5,1.0,1.5,2.0}k \in \{0.5, 1.0, 1.5, 2.0\},挑能让样本外夏普最大的值,同时要求标签分布不要失衡(比如没有哪个类超过 70%)。

纵向障碍

纵向障碍 hh 是最大持仓期。它是“放弃”式平仓。如果到 hh 时还没触发任一障碍,就在第 hh 天的收盘价平仓。

hh 的选择是战略决策,不是超参数。它编码了你的耐心程度。短的 hh(1–5 天)适合高频策略。长的 hh(20–60 天)适合波段或仓位策略。很长的 hh(几个月)适合慢动组合。

hh 很长有两个问题:(a) 纵向障碍成为主导平仓方式,上下障碍变得无关紧要;(b) 路径依赖变得更难计算(你需要检查整条路径)。h>20h > 20 时,可以考虑用日频抽样的检查,而不是逐 tick 检查。

有一个细节:纵向障碍应该按事件单独设置,不是全局设置。如果你的入场时点 t0t_0 有已知的持仓期(因为你入场是对特定信号的反应),就用那个持仓期作为纵向障碍。这让标签和具体入场时点相关。

(AFML, §3.3。)

对称与不对称的收益

一个有用的诊断:计算每种障碍触发时的平均收益。

  • 上障碍触发的平均收益:大约 k⋅σtk \cdot \sigma_t(按构造)。
  • 下障碍触发的平均收益:大约 −k⋅σt-k \cdot \sigma_t。
  • 纵向障碍触发的平均收益:应该接近零,但不严格为零(因为路径可能先触及障碍然后反弹)。

如果纵向障碍触发的平均收益明显为正或为负,障碍设置就有问题。纵向障碍应该是“都没触发”的情况,也就是价格在两个障碍之间游走。

一个常见错误:纵向障碍设得太近,导致它是主导平仓。如果 80% 的标签都是纵向触发,障碍就太紧了。放松 kk,直到三种平仓类型更平衡(比如 30% 上、30% 下、40% 纵向)。

并发与唯一性

三重障碍方法产生的标签会有重叠,唯一性权重方案(上一篇文章)正好解决这个。如果你的事件纵向障碍是 h=5h = 5,那么两个开始时间在 5 天以内的事件,标签窗口就会重叠。唯一性权重会给这些冗余观测降权。

一个有用的预处理:把事件过滤掉,保证没有两个事件相隔小于 hh 天。这会彻底消除重叠,得到一个更干净的数据集。代价是丢掉一些观测;好处是每条标签都是独立的。

哪种更好:过滤事件消除重叠,还是用唯一性权重?过滤更干净但会丢数据。权重保留数据,但会在加权样本中引入相关性。对大多数流水线,数据足够时优先用过滤;数据稀缺时再用权重。

三重障碍标签能带来什么

三个具体好处。

好处 1:回测与实盘表现一致。 如果实盘策略在障碍触发时平仓,回测也应该这样。否则,回测就是假设性的。三重障碍标签让回测只在“应该假设”的部分(滑点、成交不确定性)保持假设,不会在平仓逻辑上撒谎。

好处 2:更现实的夏普估计。 基于障碍的策略,在同样的底层信号上,夏普通常低于固定时间窗口策略,因为障碍会截断盈利交易,让亏损交易跑到超时。固定时间窗口下夏普 1.5,障碍标签下可能只有 1.0。1.0 才是接近现实的那个。

好处 3:更好的特征。 当标签是 5 天收益时,基于滚动窗口的特征信息量较小;当标签是障碍触发时,信息量更大。障碍触发是离散事件,原因清晰;5 天收益是嘈杂的聚合。在障碍标签上训练出的模型,特征重要性通常更可解释。

要点是:选择标签时要匹配你实际交易的方式。如果策略有止损和止盈,标签就要有止损和止盈。如果策略持有固定时间,标签可以是固定时间。模型学到什么,取决于你告诉它学什么。

一个具体对比

考虑 t0t_0 那天在 \100$入场。日波动率 1.5%。设置$k = 1$,所以上障碍在 $$101.50$,下障碍在 $$98.50$。纵向障碍在第 5 天。

五种可能路径:

路径 A:价格稳步上涨,第 3 天触及 \101.50$。标签 $+1$,收益 1.5%。

路径 B:价格第 1 天跌到 \98.50$,然后反弹,到第 5 天涨到 $$103$。标签 $-1$,收益 $-1.5%$。

路径 C:价格在 5 天里在 \99和和$101$之间震荡。标签由第 5 天的收益决定,为$+0.5%$。标签 $+1$(收益为正,但没触发障碍)。

路径 D:价格以 \100$开盘,第 2 天跌到$$99$,第 5 天反弹到 $$100.50$。标签 $+1$(第 5 天收益 $+0.5%$,小幅正收益)。

路径 E:价格第 1 天跳空跌到 \97$。标签 $-1$,收益 $-3%$。

固定时间窗口(5 天)标签:A=+1.5%+1.5\%,B=+3%+3\%,C=+0.5%+0.5\%,D=+0.5%+0.5\%,E=−3%-3\%。

三重障碍标签:A=+1+1,B=−1-1,C=+1+1,D=+1+1,E=−1-1。

两种打标签的方式给出了不同的信号。三重障碍方案捕捉到了策略的平仓逻辑:B 在止损上平仓,虽然 5 天收益为正。固定时间窗口方案把 B 当成赢家,但策略在它身上其实是亏钱的。

如果你的模型用固定时间窗口标签训练,而你的策略用三重障碍平仓,模型学的是错的东西。实盘夏普会让人失望。

常见错误

反复出现的五种错误。

错误 1:hh 设得太短。 如果 h=1h = 1 天,几乎所有标签都是纵向触发(因为上下障碍相对日波动噪声来说很紧)。标签基本就是 1 天前向收益。你还不如直接用固定时间窗口标签。

错误 2:kk 设得太小。 如果 k=0.1k = 0.1,障碍在入场价格的 ±0.15%\pm 0.15\%。大部分路径几分钟内就会触发障碍。标签噪声很大。

错误 3:用了错误的波动率估计。 如果用长期波动率(比如 5 年均值)而不是滚动波动率,障碍不会随状态变化。用 20–60 天滚动波动率。

错误 4:忽略入场和首次观测之间的时间差。 如果你在 t0t_0 计算特征,而 t0+1t_0 + 1 的价格已经突破上障碍,标签是 +1+1,但路径本身有歧义。要小心入场时点的对齐。

错误 5:对所有资产用同一个 kk。 高波动率资产需要比低波动率资产更宽的障碍。用滚动波动率缩放的障碍,也就是 k⋅σtk \cdot \sigma_t,其中 σt\sigma_t 是具体资产的。

更大的视角

三重障碍方法是“诚实打标签”大框架的一块。其他几块是:并发处理(第 4 章)、meta-labeling(第 6 章)、分数阶差分标签(第 5 章)。合起来,它们形成了一个既尊重金融数据结构、也尊重交易策略逻辑的标签流水线。

如果只从本文带走一条:你的标签应该匹配你的平仓方式。如果不匹配,你的回测就在撒谎,这个谎言会在实盘上线的第一个月暴露出来。