Quantitative Research

The Triple Barrier Method: A Better Way to Label Trades Than Using a Fixed Horizon

The triple barrier method replaces naive fixed-horizon labels with a path-aware framework that defines take-profit, stop-loss, and time-expiry barriers, then labels each event by whichever barrier is hit first.

12 min readAXLFI Blog

Overview

The triple barrier method is one of the most useful ideas in financial machine learning because it fixes a major problem in naive labeling: a return measured at a fixed future date often ignores the path. A standard label might say yₜ = sign(pₜ₊ₕ − pₜ), but that can be misleading.

A trade may hit a profit target quickly, stop out immediately, or just drift sideways until time runs out. A fixed-horizon label treats all of those paths too similarly. The triple barrier method improves this by asking: which barrier gets hit first? That is what makes it so practical.

Fixed-horizon label (naive)

yₜ = sign(pₜ₊ₕ − pₜ)

Triple barrier label

yₜ₀ = first barrier touched ∈ {+1, −1, sign(r)}

Visual

Triple Barrier Path Diagram

A synthetic price path wanders inside a box formed by an upper take-profit barrier, a lower stop-loss barrier, and a vertical time barrier. The path never touches either horizontal barrier, so the vertical barrier is hit first and the label falls back to the sign of the return at expiry.

Article Section

What the method actually is

At event time t₀, define three barriers. The upper barrier is a take-profit level, the lower barrier is a stop-loss level, and the vertical barrier is a maximum holding period. Then observe the path of price between t₀ and t₁.

The label is determined by the first barrier touched: +1 if the upper barrier is hit first, −1 if the lower barrier is hit first, and sign(pₜ₁ − pₜ₀) if neither is hit before the time limit.

the label is determined by the first barrier touched

Upper barrier (take-profit)

p_up = pₜ₀ · (1 + θ_up · σₜ₀)

Lower barrier (stop-loss)

p_dn = pₜ₀ · (1 − θ_dn · σₜ₀)

Vertical barrier (time limit)

t₁ = t₀ + h

Label rule

y = +1 (TP hit) | −1 (SL hit) | sign(r) at t₁

Article Section

Why this is better than fixed-horizon labels

A fixed-horizon label only checks the endpoint: pₜ₀₊ₕ − pₜ₀. But trades do not happen that way in reality. Suppose two trades both end flat after 10 days. Trade A goes up +5% on day 2 then comes back to flat. Trade B drops −4% immediately then recovers to flat.

A fixed-horizon label sees both as neutral. The triple barrier method does not. It sees the actual path: path matters, not just endpoint. That is a much better representation of trading reality.

path matters, not just endpoint

Article Section

Why volatility scaling matters

The price barriers are usually scaled by volatility. This matters because a 2% move means different things in different regimes. If volatility is low, 2% may be a meaningful move. If volatility is high, 2% may be noise.

Volatility scaling makes the barriers adaptive: barrier width is proportional to the local market noise level. That is one of the best parts of the method.

Volatility-scaled take-profit

p_up = pₜ₀ · (1 + θ_up · σₜ₀)

Volatility-scaled stop-loss

p_dn = pₜ₀ · (1 − θ_dn · σₜ₀)

Adaptive rule

barrier width ∝ market noise level key insight

Article Section

A simple example

Suppose pₜ₀ = 100, σₜ₀ = 0.02, and θ_up = θ_dn = 2. Then the upper barrier is 100 · (1 + 2 · 0.02) = 104, and the lower barrier is 100 · (1 − 2 · 0.02) = 96.

If the vertical barrier is 10 days, then y = +1 if price touches 104 first, y = −1 if price touches 96 first, otherwise use the sign of the return on day 10. That is the whole logic in concrete form.

Upper barrier

100 · (1 + 2 × 0.02) = 104

Lower barrier

100 · (1 − 2 × 0.02) = 96

Article Section

Why this is useful in ML

Suppose you are training a classifier to predict whether a signal is good. A naive target like sign(pₜ₊₁₀ − pₜ) mixes together: trades that worked immediately, trades that almost stopped out, trades that drifted nowhere, and trades that hit profit fast then reversed.

The triple barrier method gives a target more aligned with actual trade outcomes. It can produce classification labels yₜ ∈ {−1, 0, 1}, event end times t₁*, and meta-labeling inputs. You can use the primary side prediction separately and let the triple barrier determine whether the trade was worth taking.

Classification labels

yₜ ∈ {−1, 0, +1} based on which barrier was touched first.

Event end times

t₁* is the time of first barrier hit, useful for purged cross-validation.

Meta-labeling inputs

Primary model predicts direction; triple barrier labels whether that directional bet was successful.

Article Section

Python implementation

A complete implementation using NumPy and pandas. The core functions compute daily volatility, set vertical barriers, define events with horizontal barriers, determine barrier touches along the price path, and assign final labels based on which barrier was hit first.

triple_barrier.py — volatility & vertical barrier
import numpy as np
import pandas as pd


def get_daily_vol(close: pd.Series, span: int = 100) -> pd.Series:
    returns = close.pct_change()
    return returns.ewm(span=span, adjust=False).std()


def add_vertical_barrier(
    event_index: pd.Index,
    close_index: pd.Index,
    num_bars: int,
) -> pd.Series:
    event_locs = close_index.get_indexer(event_index)
    barrier_locs = event_locs + num_bars

    vertical_barriers = pd.Series(
        pd.NaT, index=event_index, dtype="datetime64[ns]"
    )
    valid = barrier_locs < len(close_index)
    vertical_barriers.iloc[valid] = close_index[barrier_locs[valid]]
    return vertical_barriers
triple_barrier.py — event definition
def get_events(
    close: pd.Series,
    event_index=None,
    pt_sl: tuple[float, float] = (1.0, 1.0),
    target: pd.Series | None = None,
    min_ret: float = 0.0,
    num_bars: int = 20,
    side: pd.Series | None = None,
) -> pd.DataFrame:
    if event_index is None:
        event_index = close.index

    if target is None:
        target = get_daily_vol(close)

    target = target.reindex(event_index)
    target = target[target > min_ret]

    if len(target) == 0:
        return pd.DataFrame(columns=["t1", "trgt", "side"])

    t1 = add_vertical_barrier(
        event_index=target.index,
        close_index=close.index,
        num_bars=num_bars,
    )

    if side is None:
        side = pd.Series(1.0, index=target.index)
    else:
        side = side.reindex(target.index).fillna(1.0)

    events = pd.DataFrame(
        {"t1": t1, "trgt": target, "side": side}
    )
    return events.dropna(subset=["trgt"])
triple_barrier.py — barrier touches & labeling
def apply_pt_sl_on_t1(
    close: pd.Series,
    events: pd.DataFrame,
    pt_sl: tuple[float, float] = (1.0, 1.0),
) -> pd.DataFrame:
    out = events[["t1"]].copy()
    out["pt"] = pd.NaT
    out["sl"] = pd.NaT
    pt_mult, sl_mult = pt_sl

    for loc, event in events.iterrows():
        start_price = close.loc[loc]
        end_time = event["t1"]
        path = close.loc[loc:end_time] if not pd.isna(end_time) else close.loc[loc:]
        returns = (path / start_price - 1.0) * event["side"]

        if pt_mult > 0:
            pt_hit = returns[returns >= pt_mult * event["trgt"]]
            if not pt_hit.empty:
                out.at[loc, "pt"] = pt_hit.index[0]

        if sl_mult > 0:
            sl_hit = returns[returns <= -sl_mult * event["trgt"]]
            if not sl_hit.empty:
                out.at[loc, "sl"] = sl_hit.index[0]
    return out


def get_bins(
    close: pd.Series,
    events: pd.DataFrame,
    pt_sl: tuple[float, float] = (1.0, 1.0),
) -> pd.DataFrame:
    touches = apply_pt_sl_on_t1(close=close, events=events, pt_sl=pt_sl)
    first_touch = pd.concat(
        [events["t1"], touches["pt"], touches["sl"]], axis=1
    ).min(axis=1)

    out = pd.DataFrame(index=events.index)
    out["t1"] = first_touch
    end_prices = close.reindex(first_touch.values).to_numpy()
    start_prices = close.reindex(events.index).to_numpy()
    side = events["side"].to_numpy()

    out["ret"] = (end_prices / start_prices - 1.0) * side
    out["bin"] = np.sign(out["ret"])
    vertical_only = first_touch.eq(events["t1"])
    out.loc[vertical_only & (out["ret"] <= 0), "bin"] = 0
    return out
usage_example.py
# Usage
close = df["close"]
events = get_events(
    close=close,
    pt_sl=(2.0, 1.0),
    min_ret=0.005,
    num_bars=20,
)
labels = get_bins(close=close, events=events, pt_sl=(2.0, 1.0))

print(labels[["ret", "bin"]].head(10))

Article Section

A practical rule set

A straightforward implementation workflow: define event times from signals, filters, or sampled observations. Estimate volatility as the rolling standard deviation of returns. Set horizontal barriers scaled by volatility. Set the vertical barrier as a maximum holding period. Scan forward along the price path and assign the label based on the first barrier touched.

Define event timesEstimate σₜSet TP / SL barriersSet vertical barrierScan forward pathAssign label

Article Section

Why this is not perfect

The method is strong, but not magical. It depends on how event times are chosen, how volatility is estimated, how wide the barriers are, and how long the vertical barrier is. Bad choices here produce bad labels.

If the barriers are too tight, labels become noisy. If the barriers are too wide, many events expire at the vertical barrier. So the method still requires judgment.

the method still requires judgment on barrier width and volatility estimation

Barriers too tight

Labels become noisy because random fluctuations trigger barriers. Many false signals.

Barriers too wide

Most events expire at the vertical barrier. The horizontal barriers rarely contribute, defeating the purpose.

Bad volatility estimate

If the volatility window is too short or too long, barriers will be miscalibrated for the current regime.

Article Section

The deeper insight

The most interesting part of the triple barrier method is that it formalizes trade outcomes in a way that resembles real execution. Most labeling schemes ask: where is price later? The triple barrier method asks: what happened first?

That is a much better question. A stop-loss hit on day 2 is not the same as a flat return on day 20. The path contains information, and the triple barrier method preserves more of it.

Naive labeling question

Where is price later?

Triple barrier question

What happened first? better question

Conclusion

Why the framework still holds up

The triple barrier method is one of the best labeling frameworks because it replaces a naive endpoint label with a path-aware trade outcome.

Set a take-profit barrier, a stop-loss barrier, and a time limit, then label the event by whichever barrier is hit first. That makes the label much closer to how real trades behave and much more useful for supervised learning, signal evaluation, and meta-labeling.

The method is not a forecasting model. It is a better way to define the target variable that your model trains on. Combined with volatility scaling and meta-labeling, it produces labels that capture what actually matters in live trading: which outcome came first, and was the signal worth acting on.