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
The link to meta-labeling
Suppose your primary model predicts direction: sideₜ ∈ {−1, 1}. The triple barrier method can then label whether that directional bet was successful. Define returns relative to the predicted side: rₜ = sideₜ · (pτ / pₜ₀ − 1). Then apply the barriers to the signed return path.
Now the label is no longer “up or down?” It becomes “was this signal worth trading?” That is a much better setup for meta-labeling.
Signed return
rₜ = sideₜ · (pτ / pₜ₀ − 1)
Meta-label question
Was this signal worth trading? meta-labeling
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.
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
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"])
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
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.
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.