Skip to content
FactorQX
intermediatebacktestingresearchpitfalls

Avoiding Lookahead Bias in Backtests

Lookahead bias quietly inflates backtest results. Learn where it sneaks in and concrete techniques to keep your simulations honest.

2 min read

A backtest is only as trustworthy as its assumptions. Lookahead bias — using information that would not have been available at decision time — is the most common way an honest-looking strategy turns out to be fantasy.

This article teaches simulation methodology. It is not investment advice and does not describe any tradeable strategy.

What lookahead bias is

Your simulation makes a decision on bar t. Lookahead bias occurs whenever that decision uses data from bar t that wasn't fully known until after the decision moment — or data from a future bar entirely.

Classic sources:

  • Deciding on the close of bar t but filling at the open of bar t (you couldn't have known the close yet).
  • Indicators computed over the full dataset, then sliced — leaking future statistics (e.g. a normalization using the global mean).
  • Survivorship: testing only instruments that exist today.

A safe decision/execution split

The cleanest defense is an explicit rule: decide on closed bars, execute on the next bar.

next_bar_execution.py
import pandas as pd
 
def simulate(df: pd.DataFrame, signal: pd.Series) -> pd.Series:
    # signal is computed from data up to and including each bar's close.
    # Shift it forward one bar so we act on the NEXT bar's open.
    position = signal.shift(1).fillna(0)
    bar_return = df["open"].pct_change().shift(-1)  # open-to-open return
    return position * bar_return

The shift(1) is the whole point: a signal known at the close of bar t can only influence the position from bar t+1 onward.

Rolling, not global, statistics

Any feature that standardizes or ranks data must use a trailing window, never the full series:

# Wrong: uses the entire history, including the future.
z_bad = (x - x.mean()) / x.std()
 
# Right: only past information at each point.
roll = x.rolling(window=100)
z_good = (x - roll.mean()) / roll.std()

A checklist

Before trusting a result, confirm:

  1. Signals are shifted relative to fills (no same-bar close-to-close magic).
  2. Every feature uses trailing windows only.
  3. Costs (fees, slippage) are modeled, even if approximate.
  4. The instrument universe reflects what existed at the time, not today.
  5. Parameters were not tuned on the same data you're reporting on.

Lookahead bias rarely announces itself — equity curves just look "too good." Building the decision/execution split into your engine from day one makes the honest version the default.

Educational content. This article covers software development and research methods only. It is not investment advice, a trading signal, or a recommendation. See our disclaimer.