Labeling Methods¶
ML4T Engineer provides 7 labeling methods for supervised learning in finance, implementing the full workflow from Advances in Financial Machine Learning (De Prado, 2018).
Overview¶
| Method | Function | Use Case |
|---|---|---|
| Triple-barrier | triple_barrier_labels() |
Fixed profit/loss targets with time limit |
| ATR-based barriers | atr_triple_barrier_labels() |
Volatility-adjusted targets |
| Rolling percentile | rolling_percentile_binary_labels() |
Adaptive threshold from return distribution |
| Fixed time horizon | fixed_time_horizon_labels() |
Simple forward returns |
| Trend scanning | trend_scanning_labels() |
Optimal horizon via t-statistic |
| Meta-labeling | meta_labels() + compute_bet_size() |
Bet sizing for primary model |
| Calendar-aware | calendar_aware_labels() |
Session-break handling for futures |
All methods return a Polars DataFrame with standardized output columns. Performance is ~50,000 labels/second via Numba-accelerated kernels.
Book: ML for Trading, 3rd ed. — Ch7
03_label_methods.pywalks through all 7 methods on real ETF data with visualizations. All case study02_labels.pynotebooks apply these methods in production pipelines.
Use the Book Guide for the broader mapping from Chapter 7
and case-study 02_labels.py files to the production labeling APIs.
Choosing a Method¶
Need fixed profit/loss targets?
├── Yes → Do barriers scale with volatility?
│ ├── Yes → atr_triple_barrier_labels()
│ └── No → triple_barrier_labels()
├── No → Need directional labels (long/short)?
│ ├── Yes → rolling_percentile_binary_labels()
│ └── No → Need optimal holding period?
│ ├── Yes → trend_scanning_labels()
│ └── No → fixed_time_horizon_labels()
Have a primary model? → meta_labels() for bet sizing
Trading futures with session breaks? → calendar_aware_labels()
LabelingConfig¶
All barrier-based methods accept a LabelingConfig object created via factory methods. This provides serialization, validation, and a bridge to DataContractConfig for pipeline integration.
Factory Methods¶
from ml4t.engineer.config import LabelingConfig
# Fixed barriers
config = LabelingConfig.triple_barrier(
upper_barrier=0.02, # 2% take profit
lower_barrier=0.01, # 1% stop loss
max_holding_period=20, # 20 bars (or "4h" for time-based)
side=1, # 1=long, -1=short, 0=symmetric
trailing_stop=False, # Enable trailing stop loss
)
# ATR-based barriers
config = LabelingConfig.atr_barrier(
atr_tp_multiple=2.0, # 2x ATR take profit
atr_sl_multiple=1.0, # 1x ATR stop loss
atr_period=14,
max_holding_period=20,
)
# Fixed horizon (simple forward returns)
config = LabelingConfig.fixed_horizon(
horizon=10,
return_method="returns", # "returns" | "log_returns" | "binary"
threshold=None,
)
# Trend scanning
config = LabelingConfig.trend_scanning(
min_horizon=5,
max_horizon=20,
t_value_threshold=2.0,
)
Serialization¶
Store labeling configurations for experiment reproducibility:
# Save to YAML
config.to_yaml("labeling_config.yaml")
# Reload
config = LabelingConfig.from_yaml("labeling_config.yaml")
Triple-Barrier Labels¶
The triple-barrier method from AFML Chapter 3 creates labels based on which of three barriers is touched first: upper (profit target), lower (stop loss), or vertical (time limit).
from ml4t.engineer.config import LabelingConfig
from ml4t.engineer.labeling import triple_barrier_labels
config = LabelingConfig.triple_barrier(
upper_barrier=0.02, # 2% profit target
lower_barrier=0.01, # 1% stop loss
max_holding_period=20, # 20 bar horizon
side=1, # Long-only signals
)
result = triple_barrier_labels(
data=df,
config=config,
price_col="close",
high_col="high", # For intrabar barrier touches
low_col="low", # For intrabar barrier touches
timestamp_col="timestamp", # Required for time-based max_holding
calculate_uniqueness=False, # Compute sample weights
uniqueness_weight_scheme="returns_uniqueness",
)
Parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
pl.DataFrame |
required | OHLCV data |
config |
LabelingConfig |
required | Barrier configuration |
price_col |
str |
"close" |
Price column for barrier calculations |
high_col |
str \| None |
None |
High column for intrabar touch detection |
low_col |
str \| None |
None |
Low column for intrabar touch detection |
timestamp_col |
str \| None |
None |
Required when max_holding_period is time-based |
calculate_uniqueness |
bool |
False |
Compute label uniqueness and sample weights |
uniqueness_weight_scheme |
str |
"returns_uniqueness" |
Weight scheme (see Sample Weighting) |
Output Columns¶
| Column | Description |
|---|---|
label |
+1 (upper hit), -1 (lower hit), 0 (vertical hit) |
label_time |
Timestamp when barrier was hit |
label_price |
Price at barrier touch |
label_return |
Return from entry to barrier |
label_bars |
Number of bars until barrier |
label_duration |
Time duration until barrier |
barrier_hit |
Which barrier: "upper", "lower", "vertical" |
label_uniqueness |
Average uniqueness (when calculate_uniqueness=True) |
sample_weight |
Sample weight (when calculate_uniqueness=True) |
The side Parameter¶
Controls directional bias:
side=1: Long-only. Upper barrier = profit, lower barrier = loss.side=-1: Short-only. Upper barrier = loss, lower barrier = profit. Labels are flipped.side=0: Symmetric. Both barriers treated equally. Label is +1 or -1 based on direction.
Trailing Stop¶
Enable a trailing stop loss that ratchets up as price moves favorably:
config = LabelingConfig.triple_barrier(
upper_barrier=0.03,
lower_barrier=0.01,
max_holding_period=20,
trailing_stop=True, # Stop loss follows highest price
)
With trailing_stop=True, the lower barrier moves up as the trade moves in favor. This reduces the time spent in losing positions.
Book: Ch7
03_label_methods.pyapplies triple-barrier labeling to SPY with visualization of barrier touches.
ATR-Based Dynamic Barriers¶
Volatility-adjusted barriers adapt to changing market conditions. The barriers scale with the Average True Range (ATR), so they're wider in volatile markets and tighter in calm markets.
from ml4t.engineer.labeling import atr_triple_barrier_labels
result = atr_triple_barrier_labels(
data=df,
atr_tp_multiple=2.0, # Take profit at 2x ATR
atr_sl_multiple=1.0, # Stop loss at 1x ATR
atr_period=14, # ATR lookback
max_holding_bars=20, # Can also be "4h" for time-based
side=1,
price_col="close",
timestamp_col="timestamp",
trailing_stop=False,
)
# Or via LabelingConfig
config = LabelingConfig.atr_barrier(
atr_tp_multiple=2.0,
atr_sl_multiple=1.0,
atr_period=14,
max_holding_period=20,
)
result = atr_triple_barrier_labels(df, config=config)
When to Use ATR Barriers¶
| Scenario | Recommendation |
|---|---|
| Single asset, stable volatility | Fixed barriers sufficient |
| Multi-asset (different volatility levels) | ATR barriers adapt per asset |
| Regime changes (calm → volatile) | ATR barriers avoid premature stops |
| Futures with varying contract sizes | ATR normalizes across contracts |
Book: CME Futures case study
02_labels.pyapplies ATR barriers on ES, NQ, and CL futures with session-aware horizons.
Rolling Percentile Labels¶
Adaptive labeling where thresholds are computed from the rolling return distribution. Produces binary long/short signals based on whether forward returns exceed a historical percentile.
from ml4t.engineer.labeling import rolling_percentile_binary_labels
result = rolling_percentile_binary_labels(
data=df,
horizon=10, # Forward return horizon (bars or "1h")
percentile=95, # 95th percentile for long signals
direction="long", # "long" or "short"
lookback_window=252 * 24, # ~1 year of hourly bars
price_col="close",
session_col=None, # Session-aware forward returns
min_samples=None, # Minimum samples for percentile
timestamp_col=None, # Required for time-based horizons
tolerance=None, # E.g., "2m" for time-based horizons
)
Output Columns¶
| Column | Example | Description |
|---|---|---|
forward_return_10 |
0.0123 | Forward return over horizon |
threshold_p95_h10 |
0.0089 | Rolling 95th percentile threshold |
label_long_p95_h10 |
1 | 1 if return exceeds threshold, 0 otherwise |
Multiple Horizons and Percentiles¶
Generate labels for multiple combinations simultaneously:
from ml4t.engineer.labeling import rolling_percentile_multi_labels
result = rolling_percentile_multi_labels(
data=df,
horizons=[5, 10, 20],
percentiles=[90, 95],
direction="long",
lookback_window=252,
)
# Produces: label_long_p90_h5, label_long_p95_h5, label_long_p90_h10, ...
Book: Ch7
03_label_methods.pycompares rolling percentile labels against triple-barrier on SPY. ETFs case study02_labels.pyuses percentile labels in its production pipeline.
Fixed Time Horizon¶
The simplest labeling method: compute forward returns over a fixed horizon. Supports both bar-count and time-based horizons.
from ml4t.engineer.labeling import fixed_time_horizon_labels
# Bar-based horizon
result = fixed_time_horizon_labels(
data=df,
horizon=10, # 10 bars forward
price_col="close",
)
# Time-based horizon
result = fixed_time_horizon_labels(
data=df,
horizon="1h", # 1 hour forward
timestamp_col="timestamp",
price_col="close",
)
Output includes a forward_return column. For binary labels, use the threshold parameter or rolling_percentile_binary_labels for adaptive thresholds.
Trend Scanning¶
De Prado's trend-scanning method finds the optimal holding period for each observation by fitting linear regressions over a range of horizons and selecting the one with the highest t-statistic.
from ml4t.engineer.labeling import trend_scanning_labels
result = trend_scanning_labels(
data=df,
min_horizon=5,
max_horizon=20,
t_value_threshold=2.0,
price_col="close",
)
Output includes trend_label (+1/-1/0), optimal_horizon, and t_statistic. Observations with |t-statistic| below the threshold receive label 0 (no trend).
Book: Ch7
03_label_methods.pydemonstrates trend scanning alongside triple-barrier and percentile methods, showing how the optimal horizon varies with market conditions.
Meta-Labeling & Bet Sizing¶
Meta-labeling is a two-stage workflow (AFML Chapter 3):
- A primary model generates directional signals (+1/-1/0)
- A meta-model predicts whether the primary signal will be profitable (1/0)
- Bet sizing converts meta-model probability into position sizes
Step 1: Generate Meta-Labels¶
from ml4t.engineer.labeling import meta_labels
meta_result = meta_labels(
data=df,
signal_col="primary_signal", # +1/-1/0 from primary model
return_col="forward_return", # Actual forward returns
threshold=0.0, # Minimum return for "correct"
)
# Adds "meta_label" column: 1 if signal was correct, 0 otherwise
Step 2: Train Meta-Model¶
Train any classifier on the meta-labels to predict P(primary signal is correct).
Step 3: Compute Bet Sizes¶
from ml4t.engineer.labeling import compute_bet_size, apply_meta_model
# Low-level: get bet size expression
bet_expr = compute_bet_size(
probability="meta_probability", # Column name or pl.Expr
method="sigmoid", # "linear" | "sigmoid" | "discrete"
scale=5.0, # Sigmoid steepness
threshold=0.5, # Minimum probability to bet
)
# High-level: apply meta-model to size positions
result = apply_meta_model(
data=df,
primary_signal_col="signal",
meta_probability_col="meta_prob",
bet_size_method="sigmoid",
scale=5.0,
threshold=0.5,
output_col="sized_signal", # signal * bet_size
)
Bet Sizing Methods¶
| Method | Formula | When to Use |
|---|---|---|
"linear" |
max(0, p - threshold) / (1 - threshold) |
Simple, interpretable |
"sigmoid" |
2 / (1 + exp(-scale * (p - 0.5))) - 1 |
Smooth, differentiable |
"discrete" |
1 if p >= threshold else 0 |
Binary position sizing |
Book: Ch7
03_label_methods.pyimplements the complete meta-labeling workflow: primary model signals → meta-labels → bet sizing on SPY.
Calendar-Aware Labels¶
For futures and other instruments with defined trading sessions, calendar-aware labeling respects session boundaries. A label that would span an overnight gap is handled correctly.
from ml4t.engineer.labeling import calendar_aware_labels
result = calendar_aware_labels(
data=df,
config=config, # LabelingConfig
calendar="CME_Equity", # "NYSE", "CME_Equity", etc.
price_col="close",
timestamp_col="timestamp",
)
The calendar prevents forward returns from crossing session breaks (e.g., CME overnight gap from 4:00 PM to 5:00 PM CT).
Sample Weighting¶
AFML Chapter 4 addresses the problem of overlapping labels creating correlated samples. The library provides the full toolkit:
Label Uniqueness¶
from ml4t.engineer.labeling import (
build_concurrency,
calculate_label_uniqueness,
calculate_sample_weights,
)
# Count how many labels overlap each bar
concurrency = build_concurrency(
event_indices=starts, # Label start indices
label_indices=ends, # Label end indices
n_bars=len(df),
)
# Average uniqueness per label (range [0, 1])
uniqueness = calculate_label_uniqueness(
event_indices=starts,
label_indices=ends,
n_bars=len(df),
)
# Combine uniqueness with return magnitude
weights = calculate_sample_weights(
uniqueness=uniqueness,
returns=returns_array,
weight_scheme="returns_uniqueness",
# Options: "returns_uniqueness", "uniqueness_only", "returns_only", "equal"
)
Sequential Bootstrap¶
The sequential bootstrap (AFML Chapter 4) draws samples while accounting for label overlap, producing a more independent training set:
from ml4t.engineer.labeling import sequential_bootstrap
selected = sequential_bootstrap(
starts=start_indices,
ends=end_indices,
n_bars=len(df),
n_draws=1000, # Number of samples to draw
with_replacement=True,
random_state=42,
)
# selected: array of indices for training
Integrated Computation¶
Triple-barrier labels can compute uniqueness and weights in a single call:
result = triple_barrier_labels(
df,
config=config,
calculate_uniqueness=True,
uniqueness_weight_scheme="returns_uniqueness",
)
# Result includes label_uniqueness and sample_weight columns
Label Statistics¶
Quick summary of label balance:
from ml4t.engineer.labeling import compute_label_statistics
stats = compute_label_statistics(df, label_col="label")
# Returns: {"n_samples", "n_positive", "n_negative", "n_neutral",
# "positive_ratio", "negative_ratio", "neutral_ratio"}
Book: Ch7
03_label_methods.pydemonstrates sequential bootstrap applied to triple-barrier labels, showing how it reduces effective sample size while improving independence.
Time-Based Durations¶
All labeling functions that accept max_holding_period or horizon support duration strings in addition to bar counts:
# Bar-based (integer)
config = LabelingConfig.triple_barrier(max_holding_period=20)
# Time-based (duration string)
config = LabelingConfig.triple_barrier(max_holding_period="4h")
config = LabelingConfig.triple_barrier(max_holding_period="1d")
config = LabelingConfig.triple_barrier(max_holding_period="30m")
Supported Duration Formats¶
| Format | Example | Meaning |
|---|---|---|
| Minutes | "30m" |
30 minutes |
| Hours | "4h" |
4 hours |
| Days | "1d" |
1 day |
| Combined | "1h30m" |
1 hour 30 minutes |
Utility Functions¶
from ml4t.engineer.labeling.utils import (
is_duration_string, # Check: is_duration_string("4h") → True
parse_duration, # Parse: parse_duration("1h30m") → timedelta(hours=1, minutes=30)
time_horizon_to_bars, # Convert to per-event bar counts using timestamps
get_future_price_at_time, # Price at exact time offset
)
Time-based horizons require a timestamp_col in the input DataFrame.
Performance¶
- Speed: ~50,000 labels/second (Numba-accelerated)
- Memory: Efficient vectorized implementation via Polars
- Accuracy: Exact match with AFML reference (validated at 1e-10 tolerance against mlfinpy)
Best Practices¶
-
Match barriers to transaction costs: Barriers should exceed expected round-trip costs. A 0.1% barrier with 0.05% commission leaves little net profit.
-
Handle class imbalance: Triple-barrier often creates imbalanced labels (many vertical barrier hits). Check with
compute_label_statistics()and consider adjusting barrier levels or using sample weights. -
Prevent leakage with sample weighting: Overlapping labels create correlated training samples. Use
calculate_uniqueness=Trueorsequential_bootstrap()to address this. -
Use ATR barriers for multi-asset: Fixed barriers work for single-asset studies but fail across assets with different volatility levels.
-
Time-based horizons for irregular data: If your bars are not equally spaced (e.g., volume bars), use duration strings (
"4h") instead of bar counts to ensure consistent holding periods.
See It In The Book¶
- Ch7
03_label_methods.pyfor the full comparison of labeling methods - Ch7
04_minimum_favorable_adverse_excursion.pyfor barrier behavior analysis - Case-study
02_labels.pyworkflows, especially CME Futures for ATR barriers - Book Guide for the full chapter and case-study map
Next Steps¶
- Read Dataset Builder when labeled data moves into training and CV workflows.
- Read Preprocessing if labels are part of a broader feature preparation pipeline.
- Use the API Reference for the full labeling surface and config objects.
References¶
- Lopez de Prado, M. (2018). Advances in Financial Machine Learning. Wiley. Chapters 3-4.
- Lopez de Prado, M. (2020). Machine Learning for Asset Managers. Cambridge.