Look-ahead bias in backtesting: a real example and how to catch it

Updated 15 October 2026 · 6 min read

Look-ahead bias means a backtest uses information that wouldn't have been available when the trade was made. It is the most common reason a backtest looks spectacular and then fails live. Here is a real example, reproducible with free data.

The example: a 10-day moving average on the S&P 500

Rule: hold the S&P 500 index while it closes above its 10-day simple moving average.
Data: daily closes from FRED, series SP500, 2,512 trading days from 17 Oct 2016 to 14 Oct 2026 (price index, no dividends).

sma    = close.rolling(10).mean()
signal = close > sma                  # known only at today's close
ret    = close.pct_change()           # today's return: yesterday's close → today's close

buggy  = (signal * ret)               # uses today's close to "earn" today's return
fixed  = (signal.shift(1) * ret)      # decide at today's close, earn from tomorrow
Buggy (no shift)Fixed (shift by one day)
Trades208208
Total return+25,280.93%+125.67%
Win rate99.52%43.27%
Max drawdown (trade by trade)−0.05%−19.79%
Buy-and-hold, same dates+266.19%+262.13%

Our own calculation from FRED data, re-computed two independent ways. Buy-and-hold differs slightly because the two trade logs end one trading day apart. Code: GitHub.

Why does one missing .shift(1) do so much damage? On any day the index closes above its moving average, that day was more likely to be an up day. The buggy version already knows today's close when it decides to be "in" for today's return, so it collects mostly up days. That's information no trader has at the time of the decision.

How to spot it without reading the code

Other common sources of look-ahead

A quick checklist

  1. Every signal uses data up to bar t; every fill happens at t+1 (or at t's close only if the signal is known before the close).
  2. Positions are shifted in vectorised code.
  3. Nothing is fitted on data after the decision date.
  4. Fundamentals use release dates.
  5. Fills exist in the price data on their dates and within the day's high–low range.
  6. Results survive realistic costs. In the fixed example, 50 bps per round trip turns +125.67% into −20.14%.

Check your own backtest for free

The free Backtest Sanity Checker skill runs these checks on your trade log (Python, no installs) and gives your AI agent a manual review checklist. It flagged the buggy version above as RED from the trade log and prices alone.

Educational content, not investment advice. Backtests have limitations and past or simulated results do not predict future returns. See our disclaimer.