Lag Features and Leakage Boundaries
Turning past values into predictive features is one of the most common moves in time-series machine learning, and also one of the easiest places to accidentally let a future value slip into a feature meant to only describe the past.
Prerequisites: Lag and Rolling-Window Features, Causal Padding and Look-Ahead Leakage
A "yesterday's closing price" feature sounds unambiguous, but a model trained to predict today's return using a feature labeled "lag-1 price" can easily end up using a price that was only actually available after the return it's supposed to predict — because of how the data was joined, how a corporate action was backfilled, or how a rolling window was computed with off-by-one indexing. Lag features are just past values of a variable used as predictors, and they are exactly where leakage boundaries — the precise, defensible line between "known at prediction time" and "not yet known" — get blurred most easily, because it's tempting to assume any feature with "lag" in its name is automatically safe.
Where the boundary actually sits
A lag- feature for predicting the value at time should use only information available as of some cutoff strictly before 's outcome is realized:
In plain English: the "-1" in a lag-1 feature has to correspond to genuinely available information — not a value that gets restated after the fact (like a revised economic release, an earnings figure later corrected, or a price adjusted retroactively for a stock split applied to historical data at the time you built the feature, not at the time it originally printed). The boundary isn't about the arithmetic of subtracting an index; it's about whether that exact number could have been read off a screen by someone trading live at time .
Worked example: the leakage a naive join creates
A researcher builds a feature "prior-quarter earnings surprise" by joining each trading day to the most recent quarterly earnings report as of that date, using the report's fiscal period end date as the join key. But earnings for a quarter ending March 31 aren't actually announced until late April — so for every day in April up to the announcement, the join incorrectly attaches an earnings figure the market did not yet know. A model trained on this feature looks like it can predict April stock moves from "prior" earnings, when it's actually partly reading the future. The fix is to join on the announcement date, not the period-end date — a one-line change to the join key that eliminates weeks of look-ahead bias across every quarter in the dataset. Backtested returns using the period-end join might show a Sharpe ratio of 2.1 that collapses to 0.9 once the announcement-date join is used correctly — the entire gap was leaked information, not genuine predictive power.
What this means in practice
For every lag feature, explicitly write down the exact timestamp at which that value became knowable in the real world, and verify the feature pipeline's join or shift logic actually respects that timestamp — not the timestamp a database happens to store the value under. This matters most for anything that gets revised after the fact (economic releases, earnings, some vendor "as-reported" fields) and anything computed with a rolling or expanding window, where an off-by-one error in the window boundary silently includes the current day's own outcome.
A lag feature is only as safe as the timestamp discipline behind it — the true leakage boundary is "when was this value actually knowable to someone trading live," which for revised data (earnings, macro releases) is often meaningfully later than the naive column labeled with that period.
The most common source of this leak is joining on a data field's reference period (fiscal quarter, trade date) rather than its availability date (announcement date, data vendor's timestamp) — always ask which of the two a join key represents before trusting a lag feature built from revisable data.
Related concepts
Practice in interviews
Further reading
- López de Prado, Advances in Financial Machine Learning, ch. 5