Financial Labels
q.label owns the event-to-target part of a financial machine-learning workflow. It answers four related but distinct questions:
- When is an observation worth labeling? Detect events instead of creating a highly redundant target at every bar.
- What future outcome should the model learn? Define directional, path-dependent, fixed-horizon, or conditional targets.
- How independent are those outcomes? Measure overlap between labels whose future paths share observations.
- How should dependent observations influence training? Resample or weight events without pretending that every row carries equal information.
Only the target-construction functions deliberately inspect future market paths. Event filters and event-time inputs must remain point-in-time safe, while overlap and weighting functions are training metadata rather than predictors. None of these outputs should be materialized as contemporaneous features.
All functions operate on one ordered instrument at a time, preserve event timestamps, and reject duplicate or unsorted indexes. For panel data, group by symbol explicitly and label each history independently.
Event-level tables use the event start time as their pandas index, named event_time. That index is the row key: labels, sample weights, metadata, and features selected at the same event times align automatically in pandas joins and are validated as the same rows by q.dataset.Dataset. Outcome times such as touch_time and end_time remain columns because they describe when an event resolves, not which row it is.
Function map
Event selection and time limits
These functions define when a label begins and the latest time at which its outcome may be observed. They do not assign a target class by themselves.
| Function | What it provides | Where it is useful |
|---|---|---|
q.label.cusum_filter |
Symmetric cumulative-change event detection with scalar or time-varying thresholds | Reduces routine sampling and focuses labeling on structural price moves. A volatility-scaled threshold adapts event density across regimes, but the threshold must be known at event time. |
q.label.vertical_barriers |
Event-to-endpoint mapping by bar count or elapsed time | Gives labels a maximum lifetime, supports explicit censoring, and makes bar-time versus wall-clock assumptions visible. |
Target construction
Target choice encodes the economic question presented to the model. There is no universally best label: simpler targets are easier to interpret, while path-dependent targets can better reflect trading decisions at the cost of more parameters and stronger overlap.
| Function | What it provides | Where it is useful |
|---|---|---|
q.label.fixed_horizon |
Return class at one future bar or time horizon, with an optional neutral band | A transparent baseline for directional forecasting and cross-sectional studies. It is easy to compare across experiments, but ignores the path taken before the endpoint. |
q.label.triple_barrier |
First-touch outcome across profit-taking, stop-loss, and vertical barriers | Useful when the learning target should resemble an exit policy and adapt to event-time volatility. Results depend on barrier widths and horizon, so parameter stability matters. |
q.label.meta_label |
Binary target stating whether a proposed side produced sufficient adjusted return | Separates direction from whether to trade. It is suited to secondary models for filtering or sizing primary-model decisions, not for inventing direction after the fact. |
q.label.trend_scanning |
Direction from the strongest forward regression across candidate horizons | Useful when trend duration is unknown and a single fixed endpoint is too rigid. The selected horizon is data-dependent, so retain end_time for purging and inspect t-value stability. |
q.label.prune_labels |
Iterative rare-class removal plus a class-level audit report | Prevents vanishing classes from destabilizing classification. Use it transparently and fit the pruning policy on training data; pruning can discard economically important tail events. |
Dependence and event lifetimes
Financial labels frequently overlap: one event may still be waiting for an outcome when several newer events begin. These functions expose that dependence rather than treating event rows as independent and identically distributed.
| Function | What it provides | Where it is useful |
|---|---|---|
q.label.concurrency |
Number of inclusive event lifetimes active at each observation | The quickest diagnostic for label crowding and the basis for overlap-aware return attribution. |
q.label.indicator_matrix |
Sparse observation-by-event lifetime membership | Makes the overlap structure explicit for diagnostics and resampling without allocating a dense matrix for mostly inactive events. |
q.label.average_uniqueness |
Mean inverse concurrency over each event lifetime | Quantifies how much independent information each label contributes. Values near 1 are isolated; smaller values indicate heavily shared paths. |
q.label.purging_metadata |
Running maximum endpoints and embargo-adjusted next-safe observations | Bridges labels to q.dataset splitters. It supplies event-lifetime metadata but does not construct folds itself. |
Sampling and training weights
These functions modify how labeled events enter model fitting. They should be derived inside the training workflow, aligned by event time, and kept separate from input features so their policy remains auditable.
| Function | What it provides | Where it is useful |
|---|---|---|
q.label.sequential_bootstrap |
Seeded bootstrap draws weighted by marginal uniqueness after each selection | Builds bags with less redundant event paths than ordinary uniform bootstrap. Repeated events are intentional, and the result remains a sample rather than a deterministic ranking. |
q.label.sample_weights |
Absolute event return after concurrency attribution | Gives greater influence to economically consequential outcomes without double-counting shared returns. Because it uses realized outcomes, it is training weight metadata, never a feature. |
q.label.time_decay |
Linear recency factors based on cumulative sample importance | Lets newer regimes matter more while retaining older observations. minimum_weight controls how aggressively history is discounted. |
q.label.class_balance_weights |
Inverse-frequency factors with equal total mass per observed class | Useful when majority classes dominate the loss. It changes the training objective and should not be confused with improving the underlying information content. |
q.label.combine_weights |
Aligned multiplication and optional normalization of weight components | Makes a composite weighting policy explicit. Inspect components first: a zero in any component removes that event multiplicatively. |
Choosing a target
- Start with
fixed_horizonwhen you need an interpretable baseline or want to isolate model quality from exit-policy assumptions. - Use
triple_barrierwhen the path to the outcome matters and barrier levels have a defensible economic or risk interpretation. - Use
trend_scanningwhen the relevant forward duration is genuinely unknown, then inspect which horizons are selected and whether labels are stable across nearby candidates. - Add
meta_labelonly when a separate primary process already supplies a side; the binary target answers whether to act, not which direction to choose. - Keep
touch_timeorend_timeregardless of method. Target accuracy is not trustworthy if overlapping future paths leak across validation folds.
Triple-barrier workflow
A typical workflow detects economically meaningful events, estimates a barrier width using information available at each event, and then follows each future path until a horizontal or vertical barrier is touched.
import qrt as q
spy = q.data.datasets.load("spy")
close = spy["close"]
# This volatility estimate is known at each event time. Initial warmup values
# stay missing and are excluded by the event and labeling functions.
target = close.pct_change(fill_method=None).ewm(
span=20, min_periods=20
).std()
event_threshold = target.mul(0.5).where(target.gt(0))
events = q.label.cusum_filter(close, event_threshold)
labels = q.label.triple_barrier(
close,
target,
events=events,
horizon=20,
upper=2.0,
lower=1.0,
min_target=0.002,
)horizon=20 means 20 observations after each event. A timedelta-like value such as "5D" instead chooses the first observed timestamp at least five days later. Events whose vertical horizon extends beyond available data are dropped by default; set drop_censored=False to retain them with a missing label and a "censored" barrier.
The result is indexed by event start time, named event_time, and contains:
| Column | Meaning |
|---|---|
vertical_barrier |
Scheduled maximum outcome time |
touch_time |
First horizontal touch or the vertical barrier |
barrier |
upper, lower, vertical, or censored |
target |
Return width known at event time |
return |
Underlying close-to-close return at the touch |
label |
Directional -1, 0, or 1 outcome |
Pass side= when a primary model or rule has already proposed long (1) or short (-1) intent. Barriers are then evaluated on side-adjusted returns, the result includes side and adjusted_return, and label becomes a binary meta-label indicating whether that side was profitable.
side_aware = q.label.triple_barrier(
close,
target,
events=events,
horizon="5D",
side=predicted_side,
)
# The same transformation can be applied to outcomes produced elsewhere.
binary_target = q.label.meta_label(realized_returns, predicted_side)Other target methods
Fixed-horizon labels apply a symmetric neutral band to returns at one future horizon:
fixed = q.label.fixed_horizon(
close,
5,
events=events,
threshold=0.005,
)Trend scanning fits forward regressions over several candidate bar counts and keeps the slope with the largest absolute t-value. Horizons count bars after the event, so a horizon of 5 fits 6 prices including the event itself.
trends = q.label.trend_scanning(
close,
events=events,
horizons=range(5, 21),
min_t_value=2.0,
)Rare classes can be pruned iteratively while preserving at least two classes. The second return value records every class’s initial and final frequency and the step at which it was removed:
target, pruning_report = q.label.prune_labels(
labels["label"],
min_fraction=0.05,
)
labels = labels.loc[target.index].assign(label=target)Overlapping outcomes
Labels with overlapping lifetimes are not independent. Use their touch times to measure overlap and derive training weights:
end_times = labels["touch_time"]
active = q.label.concurrency(close.index, end_times)
uniqueness = q.label.average_uniqueness(close.index, end_times)
membership = q.label.indicator_matrix(close.index, end_times)
# Reproducible bag sample favoring events with greater marginal uniqueness.
bag_events = q.label.sequential_bootstrap(
close.index,
end_times,
random_state=42,
)concurrency counts inclusive event intervals. average_uniqueness averages inverse concurrency over each interval. The sparse indicator matrix has one row per observation and one column per event; its row sums equal concurrency. Sequential bootstrap recomputes marginal uniqueness after every draw and may select an event more than once.
Weight components remain separate so the training policy is explicit:
return_weight = q.label.sample_weights(close, end_times, normalize=False)
decay = q.label.time_decay(uniqueness, minimum_weight=0.25)
class_weight = q.label.class_balance_weights(labels["label"])
weights = q.label.combine_weights(return_weight, decay, class_weight)sample_weights attributes each log return equally among active labels and takes the absolute event-level result. time_decay reaches 1 at the newest event, class_balance_weights gives every observed class equal total mass, and combine_weights multiplies aligned components before normalizing to unit mean. A zero component removes that event from the weighted sample; an all-zero combination is rejected when normalization is enabled.
Export running event endpoints for a chronological splitter, optionally with a bar-based embargo:
split_metadata = q.label.purging_metadata(
close.index,
end_times,
embargo=5,
)For each event, max_end_time is the latest label endpoint seen so far and next_safe_time is the first observation after that endpoint and the embargo. For example, an endpoint at position 10 with embargo=2 skips positions 11 and 12, making position 13 the next safe observation. Actual fold construction remains the responsibility of q.dataset.
Keep event_time, touch_time, or end_time with every target. Random splits can leak one future path into another when intervals overlap; use purging and an embargo when evaluating models on these labels.
See the API reference for complete signatures and validation rules.