Triple-barrier labels

q.label.triple_barrier follows each future close-price path until it touches a profit-taking barrier, a stop-loss barrier, or its vertical time limit. Horizontal widths are multiples of an event-specific target known at event time.

Without side, labels are directional: -1, 0, or 1. Events without a complete vertical horizon are dropped by default.

import pandas as pd

import qrt as q

close = q.data.datasets.load("spy")["close"]
close = close.loc[close.index.max() - pd.DateOffset(years=5) :]
target = close.pct_change(fill_method=None).ewm(span=20, min_periods=20).std()
events = q.label.cusum_filter(close, target.mul(0.5).where(target.gt(0)))
labels = q.label.triple_barrier(
    close,
    target,
    events=events,
    horizon=20,
    upper=2.0,
    lower=1.0,
    min_target=0.002,
)
labels.tail()
vertical_barrier touch_time barrier target return label
event_time
2026-06-15 2026-07-15 2026-06-17 lower 0.011291 -0.018375 -1
2026-06-16 2026-07-16 2026-06-17 lower 0.010996 -0.012488 -1
2026-06-17 2026-07-17 2026-06-26 lower 0.011234 -0.013620 -1
2026-06-18 2026-07-20 2026-06-23 lower 0.011150 -0.017623 -1
2026-06-23 2026-07-22 2026-07-06 upper 0.011109 0.024128 1

Barrier outcomes

touch_time is the realized event endpoint used by concurrency, uniqueness, sample-weighting, and purging utilities. vertical_barrier remains available to distinguish the scheduled horizon from the first actual touch.

labels.groupby(["barrier", "label"], observed=True).agg(
    observations=("label", "size"),
    average_return=("return", "mean"),
    average_lifetime=("touch_time", lambda end: (end - end.index).mean()),
)
observations average_return average_lifetime
barrier label
lower -1 430 -0.016964 5 days 23:29:51.627906
upper 1 343 0.025218 7 days 21:54:03.148688
vertical -1 1 -0.003572 29 days 00:00:00
1 3 0.042213 29 days 00:00:00

Visualize barrier outcomes

Markers encode the directional label at event time. Each translucent band extends to the first barrier touch and uses the same long, hold, or short color as its event.

chart_start = close.index.max() - pd.DateOffset(years=1)
figure = q.plot.labels(
    close.loc[chart_start:],
    labels.loc[chart_start:],
    title="SPY triple-barrier outcomes (latest year)",
)
figure.show(renderer="notebook_connected")
Back to top