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.
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_subplotsThis 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:
| Barrier | Condition | Label |
|---|---|---|
| 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 elapse | 0 (timeout) |
Labeling process:
- For each bar
t, set barriers relative toclose[t]. - Scan forward up to
max_holdbars. - 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 dfThe 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.