Markets: A Rigorous Course

13.6 Machine-learning pitfalls: leakage, purged cross-validation, labels

Advanced about 20 min

Motivation

Machine-learning tooling assumes i.i.d. samples. Financial samples are serially dependent, labels overlap in time, and the signal-to-noise ratio is tiny (13.3). Under those conditions the standard workflow leaks information from test to training and produces confident, wrong models. López de Prado (2018, ch. 3, 4, 7) codified the main fixes.

Leakage taxonomy#

  1. Look-ahead features: any feature at tt that uses data after tt: smoothed regime probabilities (13.5), full-sample GARCH parameters, centred moving averages, fundamental data stamped with the period end rather than the publication date, index membership as of today (survivorship, M04 · 4.4).
  2. Preprocessing on the full sample: scaling, PCA, winsorising thresholds or feature selection fitted on all data and then cross-validated.
  3. Overlapping labels: a label that spans several periods shares information with neighbouring labels.
  4. Selection on the test set: tuning hyperparameters until the test score is good. This turns the test set into a training set (M04 · 4.6).

Overlapping labels#

Suppose the label for sample ii is the forward return yi=∑k=1hri+ky_i = \sum_{k=1}^h r_{i+k} over hh days, with i.i.d. daily returns of variance σ2\sigma^2, and samples are taken every day.

Proposition 13.17

corr⁡(yi,yj)=max⁡(0, 1−∣i−j∣/h)\operatorname{corr}(y_i, y_j) = \max(0,\ 1 - |i-j|/h).

Proof

yiy_i and yjy_j (say j>ij>i) share the returns rj+1,…,ri+hr_{j+1},\dots,r_{i+h}, of which there are max⁡(0,h−(j−i))\max(0, h - (j-i)). Covariance is σ2\sigma^2 times that count; each variance is hσ2h\sigma^2.∎

With h=20h = 20, a sample and its neighbour have label correlation 0.95. Ordinary k-fold puts these near-duplicates on both sides of a fold boundary, and a flexible model (a forest, a boosted tree, nearest neighbours) memorises the training neighbour and "predicts" the test sample. The effective number of independent labels is roughly N/hN/h, not NN.

Purged k-fold with embargo#

Each sample ii carries an interval [t0,i,t1,i][t_{0,i}, t_{1,i}]: the time its features are observed and the time its label is resolved. Samples are sorted by t0t_0. Split positions into KK contiguous test blocks. For a test block BB let Tstart=t0,min⁡BT_{\text{start}} = t_{0,\min B} and Tend=max⁡j∈Bt1,jT_{\text{end}} = \max_{j\in B} t_{1,j}.

  • Purge: a sample i∉Bi\notin B may be used for training only if
    t1,i<Tstartort0,i>Tend.(13.28)t_{1,i} < T_{\text{start}} \quad\text{or}\quad t_{0,i} > T_{\text{end}}. \tag{13.28}
    Then no training label interval intersects any test label interval.
  • Embargo: also drop the ⌈ϵN⌉\lceil \epsilon N\rceil positions immediately after the block. Training samples just after the test block have features (trailing windows, serially correlated inputs) that overlap the test period; the embargo removes them. A typical ϵ\epsilon is 0.01.

Worked example

N=1000N = 1000 daily samples, labels are 20-day forward returns (t1=t0+20t_1 = t_0 + 20, clipped at the end), K=5K=5, ϵ=0.01\epsilon = 0.01 (embargo 10 samples). Each test block has 200 positions. For the middle fold (positions 400–599): before the block, training requires i+20<400i + 20 < 400, so positions 380–399 are purged; after the block, Tend=619T_{\text{end}} = 619, so positions 600–619 are purged, which also covers the 10-position embargo. The training set has 760 samples instead of 800. The first and last folds lose 20 each (780 training samples). Ordinary k-fold on the middle fold has 420 (train, test) pairs with overlapping label intervals; purged k-fold has none. The reference purged_kfold_splits and count_leaks reproduce these counts; test_m13_purged_cv.py checks the same purge logic on a smaller case (N=500N = 500, 10-day labels, middle fold 200–299).

Walk-forward evaluation (train on the past, test on the next block, roll) avoids training on the future altogether but uses each observation for testing only once and in one historical path. Purged k-fold uses all data for testing, at the cost of training on data from after the test period, which is acceptable only if the data-generating process is stable. Combinatorial purged CV (López de Prado 2018, ch. 12) generates many backtest paths and feeds the probability-of-backtest-overfitting analysis in M04 · 4.6. sklearn.model_selection.TimeSeriesSplit does not purge or embargo.

Triple-barrier labels#

Fixed-horizon labels ("sign of the 10-day return") ignore the path: a trade that hit its stop on day 2 and recovered by day 10 is labelled a win. The triple-barrier method labels event ee by the first of three barriers touched by Re,t=ln⁡(Pt/Pe)R_{e,t} = \ln(P_t/P_e):

label={+1Re,t≥pt first,−1Re,t≤−sl first,0vertical barrier t=e+v reached first.(13.29)\text{label} = \begin{cases}+1 & R_{e,t}\ge \text{pt first},\\ -1 & R_{e,t}\le -\text{sl first},\\ 0 & \text{vertical barrier } t = e+v \text{ reached first.}\end{cases} \tag{13.29}

In practice pt and sl are multiples of a volatility estimate known at ee (for example the GARCH forecast of 13.4), so barriers widen in stressed markets. The resolution time t1,et_{1,e} is exactly the label end time that purging needs.

Examplethe test's path

Prices 100, 101, 103, 106, 104, 99, 96, 97, 98, 100 at positions 0–9, barriers ±5%\pm5\% in log terms, vertical 4 bars. Event 0: ln⁡(106/100)=0.058\ln(106/100) = 0.058 at t=3t=3, label +1. Event 3 (106): ln⁡(99/106)=−0.068\ln(99/106) = -0.068 at t=5t=5, label −1. Event 5 (99): the path 96, 97, 98, 100 stays between −0.031-0.031 and +0.010+0.010 in log return, inside both barriers, so the vertical barrier at t=9t=9 gives 0. Event 7: vertical barrier clipped to t=9t=9, label 0.

Meta-labelling#

A primary model (a rule from M04 · 4.2, or a discretionary view) decides the side: long or short. A secondary model is trained on the meta-label

me=1{sidee⋅labele>0},(13.30)m_e = \mathbf 1\{\text{side}_e\cdot\text{label}_e > 0\}, \tag{13.30}

that is, "was acting on this signal profitable?". Its predicted probability decides whether and how much to trade. This separates direction (where domain knowledge lives) from sizing (where statistical learning helps), and it raises precision at the cost of recall.

Example in R-multiples (M03): a breakout rule with precision 40%, average win 2R and average loss 1R has expectancy 0.4×2−0.6×1=0.2R0.4\times2 - 0.6\times1 = 0.2R per trade. If a meta-model discards half the signals and the kept half has precision 52% with the same payoff sizes, expectancy per kept trade becomes 0.52×2−0.48=0.56R0.52\times2 - 0.48 = 0.56R, and expectancy per original signal is 0.28R0.28R. Whether that survives depends on the meta-model's own out-of-sample validity: it is another model fitted on the same history, and the multiple-testing penalty of M04 · 4.6 applies to it too.

Practitioner notes

  • The single most common leak in retail ML for trading: computing features with rolling(...).mean() and labels with shift(-h) in one DataFrame, then shuffling rows into k-fold. Do not shuffle; purge.
  • Report the number of independent labels. Ten years of daily data with 20-day labels is about 125 independent observations, not 2,500.
  • Prefer few, economically motivated features. With R2R^2 around 0.5% (13.3), any model with enough capacity to fit noise will.

Create a free account

Reading is open to everyone. An account keeps your progress, answers, notes and code drafts, and gives you the study plan, the journals and the simulator. Running code needs an account approved for it.