三重障碍标注法:给金融机器学习一个不作弊的标签
「未来 20 日收益为正就标 1」——这个看起来无害的标注方式,是大多数金融 ML 项目失败的第一块多米诺骨牌:固定时间窗忽略路径(20 日赚 1% 收尾但中途浮亏 15% 也算「成功」),固定收益阈值让标签分布随波动率状态漂移(低波动期全是 0、高波动期全是 ±1,模型学的是波动状态不是方向)。López de Prado 的三重障碍法一次修掉这两个病灶:止盈、止损、最大持有期三道障碍先触先得,障碍宽度按 EWMA 波动率自适应缩放。合成数据实测:固定 ±5% 阈值下标签分布从低波动期的『超时占 27%』漂移到高波动期的『超时占 12%』,自适应阈值在两种状态下几乎不变;同一特征集同一逻辑回归,三重障碍标签的样本外概率校准更贴对角线,配套障碍退出把单笔最差浮亏从 -21.8% 截到 -15.4%——标签定义与退出规则一致,模型学的才是你要执行的东西。第四节直面代价:区间重叠让样本非独立,标准 K-fold 会把泄漏伪装成 IC——这正是唯一性加权与 Purged CV 的动机(中高阶)。
一句话版本#
三重障碍标注法用「止盈障碍、止损障碍、最大持有期」三道墙给每个信号打标签——先碰到哪道墙就按哪道墙定标签,障碍宽度随波动率自适应缩放。它让机器学习的标签第一次和你实际会执行的交易长得一样。
一、问题:固定时间窗标注错在哪#
金融机器学习的第一步是造标签。最常见的做法朴素到没人多想一秒:
特征取自第 t 日,标签 = 第 t+20 日收益的符号。涨了标 1,跌了标 0。
这个标注方式有两个致命病灶,各自足以让整个项目白干。
病灶一:它忽略路径。 假设某次信号后的 20 日里,价格先跌 15%、再涨回来、最终收在 +1%。固定窗标注给它打 1——「成功」。但现实中你大概率活不到第 20 天:风控砍仓、保证金追缴、或者你自己在 -15% 的位置心态崩了。标签说这是笔好交易,你的账户说不是。模型学习的目标函数和你的实际盈亏之间出现了系统性错位。
病灶二:固定收益阈值让标签分布随波动率漂移。 有人意识到「符号太噪声」,改成「20 日收益超过 +5% 标 1、低于 -5% 标 -1、中间标 0」。听起来更稳健,实际引入了新问题:±5% 在低波动期是罕见事件,在高波动期是家常便饭。于是低波动期的样本几乎全是 0,高波动期的样本几乎全是 ±1。
用合成的双状态市场(低波动 0.8%/日、高波动 2.2%/日,马尔可夫切换)实测固定 ±5% 阈值:
| 市场状态 | 标 −1 | 标 0(超时) | 标 +1 |
|---|---|---|---|
| 低波动期 | 30% | 27% | 43% |
| 高波动期 | 45% | 12% | 43% |
标签的类别分布跟着波动率状态整体漂移。模型看到这样的数据,最省力的作弊方式是先学会识别波动率状态、再根据状态猜标签分布——它对方向的预测能力可以为零,交叉熵照样下降。你以为模型在学 alpha,其实它在学 GARCH。

右图是波动率自适应阈值(下一节介绍)下的同一统计:两种波动状态下标签分布几乎一致。模型没有捷径可走,只能去学方向本身。
二、三重障碍:先触先得#
López de Prado 在《Advances in Financial Machine Learning》里给出的修复方案,结构简单到可以画在餐巾纸上。对每个信号时刻 ,竖起三道障碍:
- 上障碍(止盈):,触及标 +1
- 下障碍(止损):,触及标 −1
- 垂直障碍(最大持有期): 日,到期未触及横向障碍,按到期收益符号标注(或直接标 0)
先碰到哪道墙,就按哪道墙定标签。 其中 是 EWMA 日波动率, 是宽度倍数, 把日波动缩放到持有期尺度——这保证障碍宽度和「h 日内典型波动幅度」同数量级,低波动期障碍自动收窄、高波动期自动放宽。

import numpy as np
import pandas as pd
def ewma_vol(ret: pd.Series, lam: float = 0.94) -> pd.Series:
"""EWMA 日波动率(RiskMetrics 权重)"""
var = ret.pow(2).ewm(alpha=1 - lam, adjust=False).mean()
return np.sqrt(var)
def triple_barrier_label(price: pd.Series, t0: int, sigma: float,
pt_mult: float = 2.0, sl_mult: float = 2.0,
max_hold: int = 20):
"""
对单个事件做三重障碍标注。
返回 (触发时刻, 标签, 事件区间终点)
标签: +1 触及止盈, -1 触及止损, 到期按收益符号
"""
# 障碍宽度按持有期尺度缩放:日波动 × sqrt(h/2)
width = sigma * np.sqrt(max_hold / 2)
p0 = price.iloc[t0]
upper = p0 * (1 + pt_mult * width)
lower = p0 * (1 - sl_mult * width)
end = min(t0 + max_hold, len(price) - 1)
for t in range(t0 + 1, end + 1):
if price.iloc[t] >= upper:
return t, 1, end # 止盈先触
if price.iloc[t] <= lower:
return t, -1, end # 止损先触
# 垂直障碍:按到期收益符号
r = price.iloc[end] / p0 - 1
return end, int(np.sign(r)), end
def make_labels(price: pd.Series, events: list[int],
pt=2.0, sl=2.0, h=20) -> pd.DataFrame:
ret = price.pct_change()
sigma = ewma_vol(ret)
rows = []
for t0 in events:
t_hit, label, t_end = triple_barrier_label(
price, t0, sigma.iloc[t0], pt, sl, h)
rows.append({"t0": t0, "t_hit": t_hit,
"t_end": t_end, "label": label})
return pd.DataFrame(rows)python几个实现细节值得强调:
- 障碍宽度必须用信号时刻的波动率,不能用全样本波动率——后者是轻微但真实的前视偏差;
- 止盈和止损倍数可以不对称。 意味着你只想学「能涨两倍止损宽度才值得做」的机会,标签会更稀但更有含金量;
- 垂直障碍不是装饰。没有它,横盘期的事件会无限期悬空;它同时定义了资金占用的上限,让每笔标签对应有限的持仓周期。
三、实测:同一模型,只换标签#
标注方式的差异最终要落到样本外表现上。实验设计刻意苛刻:同一份合成数据(带弱动量结构)、同一组特征(波动率标准化的 5 日/20 日动量、均线偏离、波动水平)、同一个逻辑回归,唯一变量是标签——一组用「20 日后收益符号」,一组用三重障碍。训练测试按 60/40 时间切分,中间留 25 日 purge 间隔防重叠泄漏。

两个结果值得说:
概率校准(左图):三重障碍标签训练出的模型,样本外预测概率与实际命中率更贴近对角线。原因不神秘——三重障碍的标签本身波动率标准化过,「+1」在任何市场状态下含义一致(涨了约 2 倍持有期波动),模型输出的概率因此跨状态可比。固定窗标签的「+1」在低波动期是 +0.5% 也算、高波动期 +8% 才算,同一个预测概率在不同状态下对应完全不同的事件,校准自然散。
浮亏截断(右图):配套障碍退出的策略,单笔最差浮亏从固定窗持有的 −21.8% 截到 −15.4%。这不是模型更聪明,是退出规则和标签定义一致的机械结果——标签说「触及止损算失败」,执行时就真的在止损位离场。固定窗标注 + 固定窗持有的组合,中途浮亏是完全放任的。
这引出三重障碍法最容易被忽略的价值:它强迫标注、回测、执行三者共用同一套规则。标签定义就是退出规则,模型优化的目标就是实盘会实现的盈亏。固定窗标注的项目里,这三层往往各说各话——研究员用 20 日收益训练,回测加了 8% 止损,实盘风控又是另一套,三层规则打架时没人知道模型到底学了什么。
四、代价:重叠样本与泄漏#
三重障碍不是免费的。它引入一个固定窗标注没有(或没那么严重)的问题:标签区间重叠。

信号密集时,第 305 日的标签用到第 305–320 日的价格,第 312 日的标签用到 312–327 日——两个「独立样本」共享了一大段行情。后果有两层:
- 有效样本量虚高。1000 个重叠标签的信息量可能只相当于 200 个独立样本,任何基于 i.i.d. 假设的统计(p 值、交叉验证方差)都过于乐观;
- 标准 K-fold 交叉验证直接泄漏。随机划分会把共享同一段行情的样本一半放进训练集、一半放进测试集,模型「记住」那段行情就能在测试集拿高分——你看到的验证 IC 有相当部分是泄漏的回声。
对策是 AFML 的另外两件套:按重叠程度做样本唯一性加权(uniqueness weighting,重叠越多权重越低),以及 Purged K-Fold(训练集中删除与测试集事件区间有任何重叠的样本,外加 embargo 缓冲带)。本文实验里的 25 日 purge 间隔就是最简化版本。这两个主题各值一篇文章,此处只需记住结论:用了三重障碍而不管重叠,等于修好了标签又打开了另一扇泄漏的门。
五、参数怎么选,以及 A 股落地#
宽度倍数 :太窄(<1)标签几乎全由噪声触发,太宽(>3)退化回垂直障碍主导、路径信息丢失。经验起点是 1.5–2.5,且应该用后续的元标注/特征重要性验证而非网格搜索——对 做精细调参是把过拟合从模型端转移到标签端。
最大持有期 :由策略的信号衰变周期决定,不是自由参数。动量类信号 10–30 日,微观结构类可能只有几根 bar。 与障碍宽度通过 耦合,改 时宽度自动跟随,这是缩放项存在的意义。
垂直障碍标 0 还是按符号:二分类(按符号)样本利用率高;三分类(标 0)把「趋势没走出来」显式分离,配合元标注更干净。倾向后者,但样本少时二分类更实际。
A 股特有的坑:涨跌停会让障碍「名义触及、实际不可成交」——涨停触及止盈时你未必卖得出,跌停触及止损时你肯定卖不出。严格的做法是障碍触发后检查当日是否一字板,是则顺延到次一可成交日、用该日实际可成交价格重算标签收益。T+1 则要求 当日建仓的信号,止损障碍最早从 起生效。这两条不处理,标签会系统性高估极端行情下的可实现收益。
六、总结#
- 固定时间窗标注有两个病灶:忽略路径(中途 -15% 的「成功」交易)与标签分布随波动率漂移(模型学波动状态而非方向);
- 三重障碍用止盈/止损/最大持有期先触先得修复路径问题,用波动率自适应宽度修复分布漂移,实测标签分布跨波动状态稳定、样本外概率校准更好、单笔最差浮亏被机械截断;
- 它最大的隐性价值是让标注、回测、执行共用同一套规则——模型优化的就是你会执行的;
- 代价是区间重叠:不配合唯一性加权和 Purged K-Fold,交叉验证分数含泄漏水分;
- 参数上 取 1.5–2.5、 由信号衰变周期决定,不要对标签参数做网格搜索;A 股需处理涨跌停不可成交与 T+1。
三重障碍是 AFML 体系的地基:后面的元标注(拿它的标签训练二级模型)、唯一性加权、Purged CV 全部建立在这套事件定义之上。地基打歪,上层全歪。
本文使用合成数据演示方法论,所有数值为受控模拟结果,不构成投资建议。