Signals·Triple-Barrier Labeling·Beginner

Labeling Triple Barrier

Implement the triple-barrier labeling method from Advances in Financial Machine Learning that labels each observation based on which barrier is hit first - the profit-taking barrier, stop-loss barrier, or the maximum holding time expiration horizon.

data-labelingtrading-signals

Labeling Triple Barrier

This notebook demonstrates the implementation of Triple Barrier Labeling, a meta-labeling technique for financial time series data. It includes data generation, the labeling function, and visualization of the results.

[ ]
import pandas as pd
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots

This section imports necessary libraries for data manipulation (pandas, numpy) and advanced plotting (plotly).

1. Data Generation

Synthetic OHLCV (Open, High, Low, Close, Volume) data is generated to simulate financial time series for demonstration purposes.

[ ]
def generate_data(periods: int) -> pd.DataFrame:
    """
    Generates synthetic OHLCV price data using a geometric random walk.

    Parameters
    ----------
    periods : int
        Number of 1-minute bars to generate.

    Returns
    -------
    pd.DataFrame
        DataFrame with columns: open, high, low, close, volume, datetime.
    """
    start_date     = pd.to_datetime("2024-01-01 00:00:00+00:00")
    datetime_index = pd.date_range(start_date, periods=periods, freq="1min", tz="UTC")
    price_data = []
    last_close = 42000
    for i in range(periods):
        open_price  = last_close + np.random.normal(0, last_close * 0.0005)
        close_price = open_price + np.random.normal(0, last_close * 0.005)
        body_high   = max(open_price, close_price)
        body_low    = min(open_price, close_price)
        high_price  = max(body_high + abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
        low_price   = min(body_low  - abs(np.random.normal(0, last_close * 0.002)), open_price, close_price)
        if high_price < low_price:
            high_price, low_price = low_price, high_price
        price_data.append({
            "open":  max(1, int(open_price)),
            "high":  max(1, int(high_price)),
            "low":   max(1, int(low_price)),
            "close": max(1, int(close_price))
        })
        last_close = close_price
    df = pd.DataFrame(price_data, index=datetime_index)
    df.index.name = "datetime"
    df["volume"]   = np.random.uniform(100.0, 500.0, periods)
    df["datetime"] = df.index.to_series()
    return df.reset_index(drop=True)

The generate_data function creates a DataFrame of OHLCV data using a geometric random walk model. This synthetic data mimics realistic price movements, including volume, over a specified number of periods.

[ ]
df = generate_data(500)
display(df.head())
open high low close volume datetime
0 42019 42346 41998 42233 287.634010 2024-01-01 00:00:00+00:00
1 42219 42343 42170 42304 108.598875 2024-01-01 00:01:00+00:00
2 42303 42375 41860 42008 242.842559 2024-01-01 00:02:00+00:00
3 41984 42312 41892 42199 194.264257 2024-01-01 00:03:00+00:00
4 42197 42536 42115 42466 479.673642 2024-01-01 00:04:00+00:00

A dataset of 500 periods is generated and the initial rows are displayed to verify data structure.

2. Triple Barrier Labeling Model

This section defines and applies the Triple Barrier Labeling methodology. This technique is used to generate target labels for machine learning models in finance, providing a robust way to define profit, loss, and timeout events.

Triple Barrier Labeling is a metalabeling technique introduced by Marcos Lopez de Prado in Advances in Financial Machine Learning. It assigns labels to each bar based on which of three exit barriers price touches first.

Three barriers:

BarrierConditionLabel
Upper (profit take)Price rises by pt_multiplier × ATR+1 (profit)
Lower (stop loss)Price falls by sl_multiplier × ATR−1 (loss)
Vertical (time)max_hold bars elapse0 (timeout)

Labeling process:

  1. For each bar t, set barriers relative to close[t].
  2. Scan forward up to max_hold bars.
  3. Record the first barrier hit and assign the corresponding label.

Limitation: ATR-based barriers make labels adaptive to volatility but still require calibration per asset class. Timeout-0 labels may represent both stagnation and mild directional moves.

[ ]
def labeling_triple_barrier(
    df: pd.DataFrame,
    atr_period: int = 14,
    pt_multiplier: float = 2.0,
    sl_multiplier: float = 1.0,
    max_hold: int = 20,
) -> pd.DataFrame:
    """
    Applies triple barrier labeling to each bar in the DataFrame.

    Core logic
    ----------
    1. Computes ATR(atr_period) for dynamic barrier sizing.
    2. For each bar t:
       a. Computes upper barrier = close[t] + pt_multiplier × ATR[t]
          and lower barrier = close[t] - sl_multiplier × ATR[t].
       b. Scans forward bars t+1 … t+max_hold.
       c. Assigns label +1 if upper barrier hit first, -1 if lower hit first,
          0 if neither barrier is hit within max_hold bars.

    Parameters
    ----------
    df : pd.DataFrame
        OHLCV DataFrame.
    atr_period : int
        ATR computation period.
    pt_multiplier : float
        Profit-take barrier as ATR multiple.
    sl_multiplier : float
        Stop-loss barrier as ATR multiple.
    max_hold : int
        Maximum bars to hold before timeout.

    Returns
    -------
    pd.DataFrame
        DataFrame with: atr, upper_barrier, lower_barrier, label (signal).
    """
    df = df.copy().sort_values("datetime", ignore_index=True)

    # ── ATR ───────────────────────────────────────────────────────────────────
    tr = pd.concat([
        df["high"] - df["low"],
        (df["high"] - df["close"].shift(1)).abs(),
        (df["low"]  - df["close"].shift(1)).abs(),
    ], axis=1).max(axis=1)
    df["atr"] = tr.rolling(atr_period).mean()

    df["upper_barrier"] = df["close"] + pt_multiplier * df["atr"]
    df["lower_barrier"] = df["close"] - sl_multiplier * df["atr"]
    df["label"]         = 0  # default: timeout

    close_arr = df["close"].values
    upper_arr = df["upper_barrier"].values
    lower_arr = df["lower_barrier"].values

    for i in range(len(df) - max_hold):
        if np.isnan(upper_arr[i]):
            continue
        ub = upper_arr[i]
        lb = lower_arr[i]
        for j in range(i + 1, min(i + max_hold + 1, len(df))):
            if close_arr[j] >= ub:
                df.at[i, "label"] = 1    # profit-take hit
                break
            elif close_arr[j] <= lb:
                df.at[i, "label"] = -1   # stop-loss hit
                break

    # Expose label as signal for consistency with other notebooks
    df["signal"] = df["label"]
    return df

The labeling_triple_barrier function implements the core logic of the triple barrier method. It calculates dynamic profit-take, stop-loss, and time barriers based on Average True Range (ATR) and then assigns a label (+1 for profit, -1 for loss, 0 for timeout) to each bar.

  • pt_multiplier × ATR: Adaptive profit target. Wider in volatile markets, narrower in quiet markets.
  • Asymmetric barriers (pt_multiplier=2.0, sl_multiplier=1.0): A 2:1 reward-to-risk ratio. A model requires greater than 33% accuracy to be profitable in expectation.
[ ]
df_signals = labeling_triple_barrier(df, pt_multiplier=2.0, sl_multiplier=1.0, max_hold=20)
print("--- Label Distribution ---")
print(df_signals["label"].value_counts())
--- Label Distribution ---
label
-1    266
 1    159
 0     75
Name: count, dtype: int64

The labeling_triple_barrier function is applied to the generated financial data. The distribution of the resulting labels (profit, loss, timeout) is then printed to provide an overview of the labeling outcome.

3. Visualization of Labels

This section provides a visual representation of the triple barrier labeling process, illustrating how barriers are set and how labels are assigned based on price interaction with these barriers.

[ ]
profit = df_signals[df_signals["label"] ==  1]
loss   = df_signals[df_signals["label"] == -1]
timeout= df_signals[df_signals["label"] ==  0]
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
    subplot_titles=["Price + Barriers + Labels", "Label Timeline"],
    row_heights=[0.7, 0.3])
fig.add_trace(go.Scatter(x=df_signals["datetime"], y=df_signals["close"],
    mode="lines", name="Close", line=dict(color="black", width=1)), row=1, col=1)
fig.add_trace(go.Scatter(x=df_signals["datetime"], y=df_signals["upper_barrier"],
    mode="lines", name="Upper Barrier", line=dict(color="green", dash="dash", width=1)), row=1, col=1)
fig.add_trace(go.Scatter(x=df_signals["datetime"], y=df_signals["lower_barrier"],
    mode="lines", name="Lower Barrier", line=dict(color="red", dash="dash", width=1)), row=1, col=1)
fig.add_trace(go.Scatter(x=profit["datetime"],  y=profit["close"],
    mode="markers", marker=dict(color="green",  size=6, symbol="circle"), name="Profit (+1)"), row=1, col=1)
fig.add_trace(go.Scatter(x=loss["datetime"],    y=loss["close"],
    mode="markers", marker=dict(color="red",    size=6, symbol="circle"), name="Loss (-1)"),   row=1, col=1)
fig.add_trace(go.Scatter(x=timeout["datetime"], y=timeout["close"],
    mode="markers", marker=dict(color="gray",   size=4, symbol="circle"), name="Timeout (0)"), row=1, col=1)
fig.add_trace(go.Scatter(x=df_signals["datetime"], y=df_signals["label"],
    mode="lines", name="Label", line=dict(color="purple")), row=2, col=1)
fig.update_layout(title_text="Triple Barrier Labeling",
    xaxis_rangeslider_visible=False, height=700, xaxis2_title="Datetime")
fig.show()

The generated labels are visualized alongside the price data and barriers. The plot displays the close price, upper and lower barriers, and markers indicating profit, loss, or timeout events. A separate subplot shows the timeline of assigned labels.

Conclusion

This notebook successfully demonstrated the implementation of Triple Barrier Labeling, a crucial technique for generating meaningful labels in financial time series data. We covered synthetic data generation, the core labeling logic, and visualized the results to understand how profit, loss, and timeout events are identified based on dynamic barriers.