Triple-Barrier Labeling

SkillCommerce & finance

Label trades using profit-target, stop-loss, and time barriers with volatility-adaptive thresholds. Use when creating supervised labels for financial time series.

Available today. Use it from your connected AI after setup.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Triple-Barrier Labeling skill

What this skill tells your AI

The instructions your AI receives, as published by ml4t/skills in features/triple-barrier/SKILL.md and read by ahel’s review.

Fixed return thresholds ignore volatility - a 2% move is noise in crypto but a signal in treasuries. Triple-barrier labels adapt to the asset's current regime.

The Problem

Naive binary labels (return > 0) are noisy and ignore position management. A trade that gains 5% then gives back 8% is labeled "winning" if you only check the endpoint. Triple-barrier labeling mirrors real trading: you exit when you hit a profit target, a stop loss, or time runs out.

The Pattern

WRONG

import numpy as np

# Fixed threshold ignores volatility regime
labels = np.where(fwd_returns > 0.02, 1, np.where(fwd_returns < -0.01, -1, 0))

CORRECT

import numpy as np

def triple_barrier_labels(
    prices: np.ndarray,
    upper_mult: float = 2.0,
    lower_mult: float = 1.5,
    atr_period: int = 14,
    max_holding: int = 10,
) -> np.ndarray:
    """Label each bar: +1 profit hit, -1 stop hit, 0 time expiry."""
    # Volatility-adaptive barriers via a TRAILING mean of absolute price changes.
    # mode="same" would centre the window and let atr[i] see bars after i.
    abs_changes = np.abs(np.diff(prices, prepend=prices[0]))
    atr = np.convolve(abs_changes, np.ones(atr_period) / atr_period)[: len(prices)]

    # NaN, not 0: the final max_holding bars have no full horizon, and labeling
    # them "time expiry" would teach the model that censoring means no move.
    labels = np.full(len(prices), np.nan)
    for i in range(len(prices) - max_holding):
        upper = prices[i] + atr[i] * upper_mult
        lower = prices[i] - atr[i] * lower_mult
        labels[i] = 0.0  # time expiry unless a barrier is touched first
        for j in range(1, max_holding + 1):
            if prices[i + j] >= upper:
                labels[i] = 1; break
            elif prices[i + j] <= lower:
                labels[i] = -1; break
    return labels  # drop the NaN tail before training

Barrier Calibration

SymptomCauseFix
90%+ stops hitBarriers too tightWiden lower_mult
90%+ time expiryBarriers too wideTighten multipliers or shorten max_holding
Label imbalance >3:1Asymmetric barriersAdjust upper/lower ratio

The ATR multiplier controls barrier width relative to current volatility. Typical ranges: upper 1.5-3.0x, lower 1.0-2.0x. De Prado's original uses EWMA daily vol; ATR is a practical alternative that captures intraday range.

MFE/MAE diagnostics: Plot Maximum Favorable Excursion (best unrealized P&L) and Maximum Adverse Excursion (worst drawdown) for each trade to calibrate barriers empirically - barriers should sit at natural break points in the MFE/MAE distributions.

Guardrails

  • Purging required: CV must purge max_holding_period bars around test boundaries to prevent leakage
  • Label overlap: labels with overlapping holding periods are not IID - effective sample size is ~N/H where H is holding period. Use sample uniqueness weighting or sequential bootstrap
  • Class balance: check label distribution - use class weights if imbalanced beyond 3:1
  • ATR lookback: must use only past data; atr[i] must not include bar i+1
  • Tie-breaking: when both barriers are crossed in the same bar, define a resolution rule (e.g., stop-loss takes priority)

Production Implementation

ml4t-engineer provides a validated, vectorized implementation:

from ml4t.engineer.config import LabelingConfig
from ml4t.engineer.labeling import atr_triple_barrier_labels

config = LabelingConfig.atr_barrier(
    atr_tp_multiple=2.0,
    atr_sl_multiple=1.5,
    atr_period=14,
    max_holding_period=10,
)
labels = atr_triple_barrier_labels(
    df,
    config=config,
    price_col="close",
    timestamp_col="timestamp",
)
# Returns: label, label_time, label_bars, label_return

Checklist

  • Barriers are volatility-adaptive (ATR or realized vol), not fixed thresholds
  • max_holding_period matches CV purge window (label_horizon)
  • Label distribution checked - no single class >80%
  • ATR computed from past data only (no lookahead)
  • Short-side labels handled correctly if strategy is long/short

Signals

GitHub stars
20
Forks
11
Last commit
Sep 2026
Advanced
Item type
skill
Key
ml4t-triple-barrier
Source
github.com/ml4t/skills