Look-Ahead Bias: The Backtest Killer
Survivorship in the universe: names trading each year, share still listed in July 2026, and median returntable ·
2026-07-31 · 10×6
Same-bar decision vs a one-session lag: SPY, average session gain, 2016-2025table ·
2026-07-31 · 10×5
The hindsight ceiling: SPY buy and hold, the same year without its biggest up days, and perfect one-day foresighttable ·
2026-07-31 · 10×5
Survivorship in the universe: names trading each year, share still listed in July 2026, and median return
Survivorship in the universe: names trading each year, share still listed in July 2026, and median return
| year | names_trading | still_listed_pct | median_return_all_pct | median_return_survivors_pct | survivor_gap_pp |
|---|---|---|---|---|---|
| 2015 | 7063 | 52.5 | -5.39 | -5.29 | 0.09 |
| 2016 | 7009 | 55.4 | 7.65 | 10.85 | 3.2 |
| 2017 | 7092 | 57.7 | 7.51 | 11.32 | 3.81 |
| 2018 | 7245 | 61.1 | -13.67 | -13.03 | 0.64 |
| 2019 | 7605 | 63.4 | 14.1 | 17.04 | 2.94 |
| 2020 | 7745 | 67.9 | 1.06 | 2.74 | 1.68 |
| 2021 | 8506 | 69.1 | 6.88 | 10.47 | 3.59 |
| 2022 | 9217 | 73.5 | -20.26 | -19.95 | 0.31 |
| 2023 | 9013 | 80.8 | 4.65 | 5.78 | 1.13 |
| 2024 | 9257 | 87.5 | 2.24 | 2.9 | 0.66 |
the exact SQL behind every number
WITH recent AS (
SELECT DISTINCT ticker
FROM global_markets.stocks_daily_aggs
WHERE date >= toDate('2026-06-15')
AND date <= toDate('2026-07-28')
),
per_name AS (
SELECT toYear(date) AS year,
ticker,
argMin(toFloat64(close), date) AS first_close,
argMax(toFloat64(close), date) AS last_close,
count() AS sessions
FROM global_markets.stocks_daily_aggs
WHERE date >= toDate('2015-01-01')
AND date <= toDate('2024-12-31')
AND close > 1
GROUP BY year, ticker
HAVING sessions >= 200
)
SELECT year,
uniqExact(ticker) AS names_trading,
round(100 * uniqExactIf(ticker, ticker IN (SELECT ticker FROM recent)) / uniqExact(ticker), 1) AS still_listed_pct,
round(100 * quantileDeterministic(0.5)(last_close / first_close - 1, cityHash64(ticker)), 2) AS median_return_all_pct,
round(100 * quantileDeterministicIf(0.5)(last_close / first_close - 1, cityHash64(ticker), ticker IN (SELECT ticker FROM recent)), 2) AS median_return_survivors_pct,
round(100 * (quantileDeterministicIf(0.5)(last_close / first_close - 1, cityHash64(ticker), ticker IN (SELECT ticker FROM recent))
- quantileDeterministic(0.5)(last_close / first_close - 1, cityHash64(ticker))), 2) AS survivor_gap_pp
FROM per_name
GROUP BY year
ORDER BY year
More from this analysisLook-Ahead Bias: The Backtest Killer
Same-bar decision vs a one-session lag: SPY, average session gain, 2016-2025
table 10×5
→
The hindsight ceiling: SPY buy and hold, the same year without its biggest up days, and perfect one-day foresight
table 10×5
→
Average S&P 500 tracker return by calendar month, May 2005 through April 2026
table 12×5
→
Survivors-only average vs whole-cohort average, by starting year
table 7×6
→
See all 2,173 queries →