Mattheus Public Preview

Join Waitlist

Mattheus Public Preview

Current Blog

How to Prevent Look-Ahead Bias in Trading Backtests

A backtest is time-valid only when every input and model decision was available at the simulated decision time.

Published Updated
Blue and violet data spheres separated by a bright time boundary, with only past observations reaching a central decision point

By alyc

Look-ahead bias enters a trading backtest when any value, filter, label, or model choice uses information that was unavailable at the simulated decision time. Prevent it by recording when each input became available, splitting data chronologically, fitting transformations only on past data, delaying labels until their observation window closes, and replaying the strategy one decision at a time.

The mistake is easy to hide because a dataset can be sorted by date and still contain future knowledge. A revised economic release may carry the date of the original release. A company classification may reflect its current status across the full history. A closing price cannot support an order assumed to fill before that close. The relevant question is not when a row is dated. It is when the value became knowable to the simulated strategy.

Start with the simulated decision time

Write down the exact point at which the strategy is allowed to decide. For a daily strategy, that might be five minutes after the close with execution at the next open. For an intraday strategy, it could be the timestamp immediately after a completed bar. Every feature, filter, universe rule, and risk input must have an availability time at or before that point.

A compact audit rule is available_at must be less than or equal to decision_at. Apply it to every field, including fields beyond market prices. Corporate actions, earnings, index membership, security identifiers, analyst data, funding rates, and venue status can all change after their nominal observation date. If the source does not preserve revision history, the backtest cannot prove what the strategy would have known.

Data lineage for trading research helps here because the source version, retrieval time, transformation path, and settings stay attached to the result. A clean lineage record does not remove bias, but it makes the time boundary inspectable.

Worked example: the row date is not its availability time

Consider two invented releases for the same economic observation. Version A is observed on June 1, published on June 2 at 08:30Z, and received by the strategy at 08:30:02Z. Version B revises that observation on June 5 at 08:30Z. A June 2 decision at 08:30:01Z cannot use either version because even A has not reached the strategy. A decision at 08:31Z can use A, but cannot use B.

Store observation time, publication time, receipt time, and version separately. For this example, available_at is the later of publication and receipt time. Select the latest version whose available_at is no later than decision_at; never join the revised value merely on the June 1 observation date.

This same test belongs on a closing price: a decision before the completed close cannot use that close, even if the resulting table is sorted chronologically.

Build point-in-time inputs

Keep the observation timestamp and the availability timestamp as separate fields. When vendors correct a record, preserve the earlier version or record the correction time instead of replacing history in place. Universe membership should also be reconstructed for each date. Using today’s surviving instruments across an older period can remove delisted names and change the test before the strategy runs.

Feature code should read only the slice available at the decision boundary. Rolling statistics need a clear window endpoint. A moving average used at the close must not include a bar that was incomplete at the decision time. A normalization parameter fitted across the entire sample gives early rows information about later values, even when the final feature table looks chronological.

Keep training and validation on the correct side of the clock

Scikit-learn documents TimeSeriesSplit for time-ordered data because ordinary cross-validation can train on future observations and evaluate on earlier ones. Its training windows expand forward, while each test fold comes later in time. The optional gap can exclude observations between training and testing when labels or features overlap across the boundary.

Chronological folds are necessary, but they do not protect preprocessing performed before the split. Scikit-learn’s data-leakage guidance says the train and test sets should be separated before preprocessing, and that fitting steps must use training data only. Scaling, feature selection, imputation, dimensionality reduction, and hyperparameter search belong inside each training fold.

Labels need the same treatment. If a label measures a return over the next five sessions, that label is not complete until the fifth session ends. Training data should stop early enough that no training label reaches into the validation period. A gap should match the actual information horizon rather than an arbitrary number copied from another test.

Run an availability-time audit

For each simulated decision, record six values.

  1. The decision timestamp and assumed execution timestamp.

  2. The latest permitted input timestamp.

  3. The availability timestamp for every source record.

  4. The fitted-through timestamp for each model and transformation.

  5. The end timestamp of every label used in training.

  6. The dataset version and code revision used for the run.

Reject the decision if any record crosses its boundary. Do not repair the result by shifting only the offending column after seeing performance. Fix the data contract, rerun the full pipeline, and keep the failed run in the audit history.

Replay the strategy one decision at a time

A replay test starts with an empty state at the beginning of the sample. It advances the clock to the first decision, exposes only records whose availability time has passed, fits or updates the model, records the proposed action, and moves forward. The final output is then compared with the vectorized backtest.

The two implementations should agree within documented tolerances. A disagreement is useful evidence. It often points to a rolling-window boundary, a forward-filled revision, a label that closes too late, or a model refitted with the full sample. A paper-first workflow can carry the same clock and data rules into forward testing before any live authority is considered.

Treat unusually strong results as an audit signal

Research published in The Review of Financial Studies found that some high-return options strategies were affected by filters that used information unavailable at portfolio formation time. The result is a useful warning. A high Sharpe ratio does not prove leakage, but it gives the reviewer a reason to inspect filters, exclusions, and data-cleaning rules closely.

Look-ahead controls should fail closed. If a source lacks an availability time, a revision history is missing, or a label window overlaps the test period, the run should be marked unverified. Guardrails before execution begin after research, but the same principle applies here. Missing evidence should stop a result from gaining authority.

The practical test is whether another person can replay one historical decision using only records that were available then. If they cannot, the backtest is not ready to support a trading decision, regardless of how polished its performance report looks.

Editorial note: This article was prepared with AI assistance and checked against the cited sources. No named human review is recorded.

Explore Markets with Infinite Context.

Explore Markets with Infinite Context.

Browse the public preview. Join the waitlist for gated trading features.

Browse the public preview. Join the waitlist for gated trading features.

Browse the public preview. Join the waitlist for gated trading features.

Dashboard interface preview