Meta-labels

q.label.meta_label asks whether a proposed long, short, or no-trade side produced enough side-adjusted return. It turns an existing directional decision into a binary target for a secondary model.

The primary model chooses the side; the meta-model learns whether to act on it. Zero side always receives label 0, and missing outcomes remain missing.

import pandas as pd

import qrt as q

close = q.data.datasets.load("spy")["close"]
close = close.loc[close.index.max() - pd.DateOffset(years=5) :]
returns = close.pct_change(5, fill_method=None).shift(-5).dropna()
fast = q.indicator.sma(close, 20)
slow = q.indicator.sma(close, 100)
side = fast.gt(slow).astype(int).sub(fast.lt(slow).astype(int)).reindex(returns.index)
meta = q.label.meta_label(returns, side, threshold=0.005)
pd.DataFrame({"return": returns, "side": side, "meta_label": meta}).tail()
return side meta_label
datetime
2026-07-10 -0.015445 1 0
2026-07-13 -0.009450 1 0
2026-07-14 -0.004722 1 0
2026-07-15 -0.009804 1 0
2026-07-16 -0.016704 1 0

Acceptance by side

A meta-label of 1 means the proposed side exceeded the required side-adjusted return. The threshold should reflect costs or a minimum economically useful outcome rather than being selected from the final test sample.

pd.crosstab(side.rename("side"), meta, normalize="index").rename(columns={0: "reject", 1: "accept"})
meta_label reject accept
side
-1 0.648026 0.351974
0 1.000000 0.000000
1 0.527745 0.472255

Visualize accepted sides

Accepted decisions retain the primary model’s long or short side. Rejected decisions appear as orange hold markers, making the secondary model’s effect visible without treating its binary target as a direction.

accepted_side = side.where(meta.eq(1), 0).rename("label")
figure = q.plot.labels(close, accepted_side, title="SPY meta-labeled moving-average sides")
figure.show(renderer="notebook_connected")
Back to top