Technical

Purged Cross-Validation for Financial ML (2026)

Build leakage-aware financial ML validation with purging, embargo and contiguous folds. Includes exact interval logic, Python and failure modes.

14 min read·

What Is Purged Cross-Validation?

In February 2018, Wiley published Marcos López de Prado's Advances in Financial Machine Learning. Chapter 7 describes a cross-validation problem that ordinary K-fold cannot see: one observation may be timestamped today while its label uses prices through next Friday.

Purged cross-validation removes training observations whose information intervals overlap the held-out test intervals. An embargo then removes a short block immediately after the test information set. The aim is to stop a fitted model receiving part of the path used to construct its test labels.

That is more precise than saying standard cross-validation never works for finance. If labels are contemporaneous and observations are genuinely independent, ordinary K-fold may be defensible. For overlapping forward labels, event-driven outcomes or features with long lookbacks, it usually is not. The primary reference is López de Prado (2018), chapter 7. His triple barrier method is a common source of variable-length label intervals.


Purging and Embargo Are Different Controls

Represent observation ii by an information interval

Ii=[tipred,tieval],I_i=[t^{pred}_i,t^{eval}_i],

where tipredt^{pred}_i is when the prediction is made and tievalt^{eval}_i is the last time used to determine its target. For a five-day forward return these are the signal date and fifth future date. For a barrier label, tievalt^{eval}_i is the first-touch or vertical-barrier time.

A training observation jj must be purged when it intersects any test interval ii:

Ij∩Ii≠∅⟺tjpred≤tievalandtjeval≥tipred.I_j\cap I_i\neq\varnothing \quad\Longleftrightarrow\quad t^{pred}_j\leq t^{eval}_i \quad\text{and}\quad t^{eval}_j\geq t^{pred}_i.

This single condition catches a training interval that starts inside the test interval, ends inside it or completely contains it. Comparing only start dates is insufficient. So is comparing every training interval with the final row of the test fold rather than with the test labels' latest evaluation time.

Purging addresses shared label information. It does not make the remaining observations independent and it does not cure serially correlated features.

An embargo is an extra buffer after the test information set. López de Prado expresses it as a fraction of observations, often illustrated with 1%. That number is not a universal default. A daily strategy with 60-day features and an intraday strategy with five-second features do not need the same gap. A clock-time embargo is often easier to defend than a row count when observations are irregular.

There is another distinction practitioners miss. Purged K-fold uses observations on both sides of a test block, so some folds train on dates later than their test dates. That can estimate performance across a roughly stable sample, but it does not mimic a one-way deployment. If structural drift is central to the question, use an expanding or rolling walk-forward split and purge the boundary. Scikit-learn's TimeSeriesSplit enforces chronological training and provides a row-based gap, but it does not infer variable label intervals for you.


A Correct Interval-Aware Splitter

Suppose daily signals run from 2 January to 31 March and each label ends between one and 10 sessions later. A February test fold cannot train on a 31 January event that resolves on 6 February. It also cannot train on a 28 February event whose target consumes the first week of March. Both overlap the held-out information set.

The splitter below uses contiguous test folds, exact pairwise interval checks and an embargo that starts after the latest test evaluation time. Prediction and evaluation times must already be sorted and aligned with the rows of XX.

import numpy as np import pandas as pd from sklearn.model_selection import BaseCrossValidator class PurgedKFold(BaseCrossValidator): def __init__(self, prediction_times: pd.Series, evaluation_times: pd.Series, n_splits: int = 5, embargo_pct: float = 0.01): if not prediction_times.index.equals(evaluation_times.index): raise ValueError("Time series must have the same index") self.n_splits = n_splits self.starts = pd.DatetimeIndex(prediction_times) self.ends = pd.DatetimeIndex(evaluation_times) self.embargo_pct = embargo_pct def split(self, X, y=None, groups=None): if len(X) != len(self.starts) or (self.ends < self.starts).any(): raise ValueError("Invalid or misaligned information intervals") rows = np.arange(len(X)) embargo = int(np.ceil(len(X) * self.embargo_pct)) for test_idx in np.array_split(rows, self.n_splits): mask = np.ones(len(X), dtype=bool) mask[test_idx] = False test_starts, test_ends = self.starts[test_idx], self.ends[test_idx] for j in rows[mask]: overlaps = ((self.starts[j] <= test_ends) & (self.ends[j] >= test_starts)).any() mask[j] = not overlaps first_after = self.starts.searchsorted( test_ends.max(), side="right" ) mask[first_after:first_after + embargo] = False yield rows[mask], test_idx def get_n_splits(self, X=None, y=None, groups=None): return self.n_splits

The pairwise loop is deliberately plain. For millions of events, use interval trees or vectorised search. A maintained library should also handle duplicate timestamps, empty folds, half-open interval conventions and duration-based embargoes. Whatever the implementation, write a test asserting that no returned training interval overlaps any test interval.

Do not calculate one score per fold and average Sharpe ratios blindly. Prefer concatenating strictly out-of-fold predictions in timestamp order, applying one documented execution rule and then calculating strategy metrics. Classification loss and trading P&L answer different questions.

Building the whole validation loop

The splitter is only the outer frame. For each fold, fit every learned operation on the surviving training rows: missing-value imputation, scaling, feature selection, probability calibration and position sizing. A global standard scaler contains test-period means and variances even when the labels are perfectly purged. Scikit-learn's Pipeline is helpful because fitting the estimator also fits its transformers on that fold.

Hyperparameter selection needs its own inner split. The outer test fold estimates generalisation; the inner purged or walk-forward folds choose tree depth, regularisation and label-policy parameters. Once the best inner configuration is chosen, refit it on the full outer training set and predict the outer test set once. Repeating the outer test after reading its result quietly turns it into another validation set.

For a strategy rather than a generic predictor, retain one out-of-fold prediction for every eligible event. Sort those predictions by decision time, apply the same lag and transaction-cost model used in production and resolve overlapping positions with a rule written before scoring. A mean AUC across folds can coexist with a poor P&L if the model ranks tiny moves correctly and large moves badly.

Keep fold diagnostics alongside scores:

  • number of test, purged, embargoed and surviving training rows;
  • earliest and latest prediction and evaluation time in each set;
  • class balance and average event concurrency;
  • feature distribution shifts and unseen categories;
  • P&L before and after costs, turnover and maximum simultaneous exposure.

If one fold loses half its training set while another loses 2%, a simple average hides unequal uncertainty. Weight observation-level losses by test count where appropriate, but do not weight away a regime in which the strategy failed. For portfolio metrics, the concatenated out-of-fold path is usually the clearest object.

Before a long run, inspect a tiny synthetic calendar by hand. Include an interval that ends at the test start, one that begins at the test end and one that surrounds the entire fold. Decide whether endpoints are closed or half-open according to when information becomes available, then encode that convention in tests. Record the convention beside the dataset. Off-by-one leakage is still leakage.


Where Leakage Survives

Purging is a boundary control, not a certificate of honest research. Five other routes remain open.

Feature construction. A centred rolling mean, full-sample z-score or revised fundamental can leak future information without touching the label. Every transformer, imputer and feature selector belongs inside the training pipeline. Fit it afresh within each fold.

Hyperparameter selection. Using the same purged folds to choose parameters and quote final performance still overfits the validation set. Use nested purged validation, or reserve a final untouched period. Keep a ledger of all model and label trials; our Sharpe ratio of pure noise explains why the winning backtest needs a multiple-testing haircut.

Cross-sectional leakage. Time purging does not separate entities. If the task is to generalise to unseen securities, issuers or customers, use grouped temporal splits. Rows from the same company on the same date can otherwise appear in both sets.

Calendar leakage. A row-based 1% embargo means 25 sessions in 2,500 daily observations but something quite different after event sampling. Set the gap from the longest relevant feature memory, execution delay and publication lag. Then run a sensitivity check.

Regime drift. Because purged K-fold can train on the future of an early test fold, it may answer a retrospective interpolation question. Walk-forward validation answers the live deployment question more directly. Report both when the difference is economically meaningful.

A practical review starts with a timeline for one row: when each raw field became available, when the feature was computed, when the order could be placed and when the label stopped observing the market. Only then choose the splitter. The wider time-series analysis guide covers stationarity and dependence checks, while the machine learning for trading guide places validation inside the full research process.


Frequently Asked Questions

What is purged K-fold cross-validation in one sentence?

A contiguous K-fold scheme that removes training information intervals overlapping a test fold and can add a post-test embargo.

Why does standard K-fold fail on financial time series?

It fails when shuffled folds split observations that share forward-looking label information, or when temporal dependence makes the validation design unlike deployment. It is not automatically invalid for every financial dataset.

How long should the embargo be?

Long enough to cover the dependence the purge does not address, stated in clock time or observations. The often-quoted 1% is an illustration, not a theorem; test a range based on feature memory and market timing.

Is scikit-learn TimeSeriesSplit the same thing?

No. TimeSeriesSplit trains only on earlier rows and has a fixed row gap. It does not purge arbitrary evaluation intervals, though you can combine the ideas in a custom splitter.

Does purging remove autocorrelation?

No. It removes overlapping information intervals. An embargo can reduce near-boundary dependence, but neither operation makes a serially dependent process independent.

Where is the canonical description?

Chapter 7 of López de Prado's Advances in Financial Machine Learning gives purging and embargo. Chapter 12 develops combinatorial purged cross-validation.

Skip the £25k programme - try the alternative

Master's programmes are slow and expensive. Quantt is a self-paced alternative. Start free with a real lesson and interview practice, then unlock 50+ courses and your personalised plan.

Free to start · No credit card required