Skip to content

Triple-Barrier Method

Fixed-horizon labels are convenient and almost always wrong. The triple-barrier method labels by what actually happened first: take-profit, stop-loss, or timeout. The labels match how the strategy will actually trade.

12 of 12 ~13 min

Triple-Barrier Method: Labels That Match How You Trade

A label is a question. The question is: “Was this trade a winner or a loser?” The choice of label determines what the model learns. Most quant ML pipelines use fixed-horizon labels: “did the price go up over the next 5 days?” It is the most common label in the literature, and it is the wrong label for most trading strategies.

The wrong label problem is silent. Your cross-validation looks fine. Your Sharpe is 1.8 in backtest. Your model goes live and the Sharpe drops to 0.4. You stare at the live trades and notice that the strategy exits on the stop-loss or take-profit threshold 70% of the time, not at the 5-day mark. Your labels were 5-day forward returns, but your exits are barrier hits. The model learned to predict 5-day returns. The strategy trades on barrier exits. These are different problems.

The triple-barrier method fixes this. It labels each observation by what actually happened first: the take-profit barrier, the stop-loss barrier, or the vertical (time) barrier. The labels match the strategy’s exit logic.

The reference is AFML, Chapter 3. The chapter is short. The implementation is straightforward. The benefits are large.

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

The Three Barriers

For each entry time t0t_0, three barriers are defined.

Upper barrier (take-profit): t0+Δtt_0 + \Delta_t where Δt\Delta_t is set such that the price hits pt0⋅(1+πup)p_{t_0} \cdot (1 + \pi_{\text{up}}). When price hits this level first, the label is +1+1 (or whatever you use for “win”).

Lower barrier (stop-loss): t0+Δtt_0 + \Delta_t where Δt\Delta_t is set such that the price hits pt0⋅(1−πdown)p_{t_0} \cdot (1 - \pi_{\text{down}}). When price hits this level first, the label is −1-1 (or “loss”).

Vertical barrier (timeout): t0+ht_0 + h where hh is the maximum holding period. When neither barrier is hit by hh, the label is determined by the sign of the return r(t0,t0+h)r(t_0, t_0 + h). This is the “neither hit” case.

The label for observation ii is:

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)

In words: the label is the sign of the price change at whichever barrier was hit first, capped at the vertical barrier.

A subtlety: barriers can be dynamic (based on rolling volatility) or static (fixed percentages). Dynamic barriers scale with recent volatility: in calm regimes, the barriers are tight; in volatile regimes, they are wider. This produces more consistent label distributions across regimes. Static barriers are simpler but produce label distributions that vary with the volatility regime (more −1-1‘s in volatile periods, more timeouts in calm ones).

The dynamic form:

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

where σt\sigma_t is the rolling realized volatility at time tt and kk is a constant (typically 1.0 to 2.0). The same constant kk on both sides makes the barriers symmetric in volatility units.

Why Fixed-Horizon Labels Fail

Four reasons, each one enough on its own.

Reason 1: They ignore the strategy’s actual exit logic.

A backtest with fixed-horizon labels assumes you hold for exactly hh days, no matter what. If your live strategy exits at the stop-loss or take-profit threshold first, the backtest is lying. The fixed-horizon return is the return if you held to the horizon. The actual strategy return is the return at the barrier hit. These can differ dramatically, especially in volatile regimes.

Reason 2: They produce unrealistic label distributions.

Suppose your labels are 5-day forward returns. The labels are roughly normally distributed around 0. They are not skewed toward ±π\pm \pi. In contrast, barrier labels have a structural asymmetry: the upper and lower barriers are at fixed distances from entry, so by the time the vertical barrier is hit, the price has moved less than the barriers would have allowed. The labels are more bimodal: lots of −1-1‘s, lots of +1+1‘s, and a tail of small returns in between.

This matters for classification. A model trained on barrier labels is learning to predict barrier hits, which are discrete events. A model trained on fixed-horizon returns is learning to predict a continuous variable, which is a different problem.

Reason 3: They are not path-aware.

Two paths can produce the same 5-day return: a smooth uptrend, or a sharp dip followed by a recovery. Fixed-horizon labels treat them identically. Triple-barrier labels distinguish them: the smooth uptrend might hit the upper barrier on day 2 (label +1+1, profit capped), while the dip-and-recovery path hits the vertical barrier with a small positive return (label depends on sign). The model learns different things from each.

Reason 4: They cannot express risk management constraints.

Most trading strategies have a maximum loss threshold. If the price moves against you by X%X\%, you exit. Fixed-horizon labels do not encode this. The strategy with a stop-loss will exit before the 5-day horizon, so the 5-day return is not realized. Triple-barrier labels encode the exit logic directly.

When Fixed-Horizon Labels Are Fine

Three cases where the simpler approach works.

Case 1: Buy-and-hold strategies. If you buy and never exit (or exit only at a fixed future date), fixed-horizon labels are correct. There are no barriers.

Case 2: Research on the cross-section. If you are studying “what predicts 5-day returns” as an academic question, not building a trading strategy, fixed-horizon labels are appropriate.

Case 3: Continuous position sizing. If your strategy continuously sizes positions based on predicted returns (rather than binary long/short decisions), fixed-horizon labels produce smoother training targets than barrier labels.

In most other cases (most trading strategies, especially those with stops or take-profits), triple-barrier is the right choice.

Side and Size: A Useful Distinction

AFML introduces a useful decomposition: the label can be split into two components.

Side: the sign of the trade. Is this a long, a short, or no position? Determined by which barrier was hit first (or the sign of the timeout return).

Size: the magnitude of the position. Should you size this trade at 100%, 50%, or 0%? Determined by the confidence in the prediction.

A primary model predicts the side (the direction). A secondary model (the meta-labeling model) predicts whether the primary model is correct. The secondary model’s output becomes the size.

This decomposition is the foundation of meta-labeling, which is covered in the next article. For now, the relevant point is that triple-barrier labels produce natural targets for both the primary and secondary models.

(AFML, §3.5.)

The Implementation

A clean implementation in 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')

The function returns labels −1-1, 00, or +1+1 for each event time. The 00 label corresponds to “barriers not hit, return smaller than min_ret”. Typically you drop these or treat them as −1-1 (no-trade) depending on your strategy.

Choosing the Barrier Widths

Two parameters: profit-taking multiple kupk_{\text{up}} and stop-loss multiple kdownk_{\text{down}}. They are usually the same, but they don’t have to be.

Symmetric barriers: kup=kdown=kk_{\text{up}} = k_{\text{down}} = k. Use this when you have no prior on asymmetric risk. The most common starting point.

Asymmetric barriers: kup>kdownk_{\text{up}} > k_{\text{down}} (or vice versa). Use this when your strategy has a directional bias. A long-only strategy might have kup=2,kdown=1k_{\text{up}} = 2, k_{\text{down}} = 1, willing to give back twice as much as you target on the upside, but cutting losses quickly. This skews the label distribution toward the strategy’s natural bias.

A reasonable starting point: k=1.0k = 1.0 (barriers at one daily volatility away from entry). For a daily-volatility of 1.5%, that’s barriers at ±1.5%\pm 1.5\%. A 5-day max-hold vertical barrier gives you 5 days to hit either.

How to choose kk: use the same cross-validation logic as any hyperparameter. Try k∈{0.5,1.0,1.5,2.0}k \in \{0.5, 1.0, 1.5, 2.0\} and pick the value that maximizes out-of-sample Sharpe, with the constraint that the resulting label distribution is not too imbalanced (e.g., not more than 70% of one class).

The Vertical Barrier

The vertical barrier hh is your maximum holding period. It is the “give up” exit. If neither barrier is hit by hh, you exit at the close of day hh.

The choice of hh is a strategic decision, not a hyperparameter. It encodes your patience. A short hh (1-5 days) is appropriate for high-frequency strategies. A long hh (20-60 days) is appropriate for swing or position strategies. A very long hh (months) is appropriate for portfolios of slow-moving positions.

Two issues with very long hh: (a) the vertical barrier becomes the dominant exit, making the upper and lower barriers irrelevant; (b) the path-dependence becomes harder to compute (you need to check the entire path). For h>20h > 20, consider using a daily-resampled check rather than a tick-by-tick check.

A subtle issue: the vertical barrier should be set per-event, not globally. If your entry time t0t_0 has a known holding period (because you entered in response to a specific signal), use that holding period as the vertical barrier. This makes the labels entry-time-specific.

(AFML, §3.3.)

Symmetric vs Asymmetric Returns

A useful diagnostic: compute the average return at each barrier hit.

  • Average return at upper barrier hit: roughly k⋅σtk \cdot \sigma_t (by construction).
  • Average return at lower barrier hit: roughly −k⋅σt-k \cdot \sigma_t.
  • Average return at vertical barrier hit: should be close to zero, but not exactly zero (because the path may have hit a barrier first and bounced back).

If the average return at vertical barrier is significantly positive or negative, something is wrong with your barrier setup. The vertical barrier should be the “neither” case, where the price wandered without hitting either barrier.

A common error: setting the vertical barrier so close that it is the dominant exit. If 80% of labels are vertical-barrier, your barriers are too tight. Loosen kk until the three exit types are more balanced (e.g., 30% upper, 30% lower, 40% vertical).

Concurrency and Uniqueness

The triple-barrier method creates label overlaps that the uniqueness-weight scheme (previous article) addresses. If your events have a vertical barrier of h=5h = 5, then two events starting within 5 days of each other have overlapping label windows. The uniqueness weights down-weight these redundant observations.

A useful pre-step: filter your events so that no two events are closer than hh days apart. This removes the overlap entirely and produces a cleaner dataset. The cost is that you are throwing away observations; the benefit is that each label is independent.

Which is better: filtering events to remove overlap, or using uniqueness weights? Filtering is cleaner but loses data. Weights preserve data but introduce correlation in the weighted sample. For most pipelines, filtering is preferred when you have enough data; weights are preferred when data is scarce.

What Triple-Barrier Labels Buy You

Three concrete benefits.

Benefit 1: Backtests that match live performance. If your live strategy exits on barrier hits, your backtest should too. Otherwise, the backtest is hypothetical. Triple-barrier labels make the backtest hypothetical only in the parts that should be hypothetical (slippage, fill uncertainty), not in the exit logic.

Benefit 2: Realistic Sharpe estimates. The Sharpe of a barrier-based strategy is generally lower than the Sharpe of a fixed-horizon strategy on the same underlying signal, because barriers cut winning trades short and let losing trades run to the timeout. A 1.5 Sharpe on fixed-horizon labels might be 1.0 on barrier labels. The 1.0 is closer to reality.

Benefit 3: Better features. Features computed over rolling windows are less informative when the label is a 5-day return than when the label is a barrier hit. The barrier hit is a discrete event with a clear cause; the 5-day return is a noisy aggregation. Models trained on barrier labels tend to produce more interpretable feature importances.

The takeaway: choose your labels to match how you will actually trade. If your strategy has stops and take-profits, your labels should have stops and take-profits. If your strategy holds for a fixed period, your labels can be fixed-period. The model learns what you tell it to learn.

A Concrete Comparison

Consider an entry on day t0t_0 at price \100$. Daily volatility is $1.5%$. You set barriers at $k = 1$, so upper barrier at $$101.50$and lower barrier at$$98.50$. Vertical barrier at day 5.

Five possible paths:

Path A: Price rises steadily, hits \101.50$on day 3. Label:$+1$, return $1.5%$.

Path B: Price dips to \98.50$on day 1, then rallies to$$103$by day 5. Label:$-1$, return $-1.5%$.

Path C: Price oscillates between \99andand$101$for 5 days. Label: based on day-5 return, which is$+0.5%$. Label: $+1$ (return is positive but no barrier hit).

Path D: Price opens at \100$, drops to $$99$on day 2, recovers to$$100.50$on day 5. Label:$+1$(day-5 return is$+0.5%$, small positive).

Path E: Price gaps down to \97$on day 1. Label:$-1$, return $-3%$.

Fixed-horizon (5-day) labels would give: A=+1.5%+1.5\%, B=+3%+3\%, C=+0.5%+0.5\%, D=+0.5%+0.5\%, E=−3%-3\%.

Triple-barrier labels give: A=+1+1, B=−1-1, C=+1+1, D=+1+1, E=−1-1.

The two labeling schemes produce different signals. The triple-barrier scheme captures the strategy’s exit logic: B exits at the stop-loss, even though the 5-day return is positive. The fixed-horizon scheme treats B as a winner, even though the strategy would have lost money on it.

If your model is trained on fixed-horizon labels and your strategy uses triple-barrier exits, your model is learning to predict the wrong thing. The live Sharpe will disappoint.

Common Errors

Five errors that come up repeatedly.

Error 1: Setting hh too short. If h=1h = 1 day, almost every label is vertical-barrier (because the upper and lower barriers are tight relative to daily noise). The labels are essentially 1-day forward returns. You might as well use fixed-horizon labels.

Error 2: Setting kk too small. If k=0.1k = 0.1, barriers are at ±0.15%\pm 0.15\% of entry. Most paths will hit a barrier within minutes. The labels are noisy.

Error 3: Using the wrong volatility estimate. If you use the long-run volatility (say, 5-year average) instead of rolling volatility, your barriers do not adapt to regimes. Use rolling 20-60 day volatility.

Error 4: Ignoring the time gap between entry and the first observation. If you compute features at time t0t_0 and the price at t0+1t_0 + 1 is already past the upper barrier, the label is +1+1 but the path is ambiguous. Be careful about entry-time alignment.

Error 5: Using the same kk for all assets. A high-volatility asset needs wider barriers than a low-volatility asset. Use rolling-volatility-scaled barriers (i.e., k⋅σtk \cdot \sigma_t where σt\sigma_t is asset-specific).

The Bigger Picture

The triple-barrier method is one piece of a larger framework for honest labeling. The other pieces are: concurrency handling (Chapter 4), meta-labeling (Chapter 6), and fractionally differentiated labels (Chapter 5). Together, they form a labeling pipeline that respects the structure of financial data and the logic of the trading strategy.

If you take only one thing from this article: your labels should match your exits. If they don’t, your backtest is lying to you, and the lie will show up in the first month of live trading.