21 — Backtest & Validate

Survivorship & Look-Ahead Bias#

BiasStatistical
◆ The PatternThe silent traps that make your backtest a fantasy

Survivorship bias: testing only on stocks that still exist today (ignoring delisted failures). Look-ahead bias: using data that wasn't available at the time of the decision. Both make backtests look better than reality.

Survivorship: Universe(t) ≠ Universe(today)
Hundreds of the S&P 500’s 2005 members are no longer in it — acquired, shrunk or bankrupt; a backtest on today’s members silently drops them.
Look-ahead: f(t) must use only data from [0, t]
Earnings announced on day t+3 can't inform a decision on day t, even if your database has it.
// Interactive — survivorship bias impact on backtest returns
Delisted %15%
Biased return—
Real return—
Data snooping: If you tested 100 strategies, 5 will look significant at p < 0.05 by pure chance. Adjust for multiple comparisons (Bonferroni) or use a holdout period you never touch.
Pattern bridge: Backtests are one case of a general pattern — we study what survived and forget the rest. Its psychological twin is hindsight bias: after the fact, the winners look obvious.
Worked example

1,000 funds with no skill at all, each year closing the worst 10% of those still alive. After five years 590 survive. Across all funds the average annual return is 0.26% — zero, as it should be. Across the survivors it is 2.16%. A database of today’s funds reports skill that never existed.

Where it misleads
  • Survivorship hides in index constituents, delisted stocks and closed funds; the data vendor’s history must include the dead.
  • The bias is largest exactly where returns are most volatile.
  • Selection on success happens in research too: only working strategies get written up.
In code
rng = np.random.default_rng(13)
funds, years = 1_000, 5
returns = rng.normal(0.0, 0.10, (funds, years))      # no fund has any skill
alive = np.ones(funds, bool)
for t in range(years):                                # each year the worst 10% of survivors close
    cut = np.quantile(returns[alive, t], 0.10)
    alive &= ~(returns[:, t] < cut)
all_avg, survivors_avg = returns.mean(), returns[alive].mean()
Python, in your browser — edit the code and run it again

Assumes import numpy as np and import pandas as pd (and SciPy where imported). Each snippet simulates or makes up its own data, as the comments say.

The same idea with a library
# Checklist — is your backtest honest?
# 1. Does your universe include delisted stocks?
# 2. Are you using point-in-time data?
# 3. Are fundamentals lagged by reporting delay?
# 4. Is there any future information leaking?
# 5. Did you optimise parameters on the test period?
# 6. How many strategies did you test? (data snooping)

# Point-in-time fundamental data
# Use 'as_of' date columns, not 'period_end'
df = df[df['report_date'] <= df['trade_date']]
Sources
  • S. J. Brown, W. Goetzmann, R. G. Ibbotson & S. A. Ross, “Survivorship Bias in Performance Studies”, Review of Financial Studies 5(4), 1992
  • E. J. Elton, M. J. Gruber & C. R. Blake, “Survivor Bias and Mutual Fund Performance”, Review of Financial Studies 9(4), 1996

Last reviewed October 2026 · How this site checks its content · Report a mistake

Check yourself

1. You backtest a strategy over 20 years on today’s S&P 500 members. What bias does this introduce?

Show answer

A. Today’s index is a list of winners. A fair test uses the members as they were on each date.

2. A fund database lists the average past return of funds that exist today. Why is it too high?

Show answer

A. Studies of mutual funds find that ignoring dead funds raises measured average returns noticeably — the losers are exactly the ones that disappear.

3. A backtest uses quarterly earnings dated at the quarter’s end, although they are published weeks later. The problem is:

Show answer

A. Use point-in-time data: each value stamped with the date it became known, not the period it describes.

Linked from 7
← Previous
Monte Carlo Simulation
Open in the full reader, with the topic sidebar