Survivorship Bias in Stock Data, Explained
Survivors-only average vs whole-cohort average, by starting yeartable ·
2026-08-22 · 7×6
Symbols relisted under a new issuer after a long silenceranking ·
2026-08-22 · 5×4
Symbols that printed a final daily bar, by yearranking ·
2026-08-22 · 10×3
The January 2019 universe, grouped by what happened to each nameranking ·
2026-08-22 · 9×4
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
Survivors-only average vs whole-cohort average, by starting year
Survivors-only average vs whole-cohort average, by starting year
| cohort_year | cohort_size | gone_count | survivors_only_pct | full_universe_pct | gap_pct |
|---|---|---|---|---|---|
| 2016 | 2728 | 969 | 209.9 | 148.2 | 61.8 |
| 2017 | 2522 | 819 | 160.8 | 117.3 | 43.4 |
| 2018 | 2669 | 824 | 121.4 | 88.7 | 32.6 |
| 2019 | 2646 | 695 | 138.1 | 112.7 | 25.5 |
| 2020 | 2669 | 639 | 87 | 73.1 | 13.9 |
| 2021 | 3416 | 957 | 64.4 | 48.4 | 16 |
| 2022 | 3378 | 694 | 34.2 | 24.9 | 9.2 |
the exact SQL behind every number
WITH
entry AS
(
SELECT
ticker,
toYear(date) AS cohort_start,
argMin(toFloat64(close), date) AS entry_close
FROM global_markets.stocks_daily_aggs
WHERE toMonth(date) = 1
AND date BETWEEN '2016-01-01' AND '2022-01-31'
AND ticker NOT IN ('SPCX')
GROUP BY ticker, cohort_start
HAVING argMin(toFloat64(close), date) >= 5
AND avg(volume) >= 250000
),
outcome AS
(
SELECT
ticker,
argMax(toFloat64(close), date) AS final_close,
max(date) AS last_bar
FROM global_markets.stocks_daily_aggs
WHERE date >= '2016-01-01'
GROUP BY ticker
)
SELECT
toString(e.cohort_start) AS cohort_year,
count() AS cohort_size,
countIf(o.last_bar < today() - 45) AS gone_count,
round(100 * avgIf(o.final_close / e.entry_close - 1, o.last_bar >= today() - 45), 1) AS survivors_only_pct,
round(100 * avg(o.final_close / e.entry_close - 1), 1) AS full_universe_pct,
round(100 * (avgIf(o.final_close / e.entry_close - 1, o.last_bar >= today() - 45)
- avg(o.final_close / e.entry_close - 1)), 1) AS gap_pct
FROM entry AS e
INNER JOIN outcome AS o ON o.ticker = e.ticker
GROUP BY e.cohort_start
HAVING countIf(o.last_bar >= today() - 45) > 0
ORDER BY e.cohort_start
More from this analysisSurvivorship Bias in Stock Data, Explained
Symbols that printed a final daily bar, by year
ranking 10×3
→
The January 2019 universe, grouped by what happened to each name
ranking 9×4
→
Symbols relisted under a new issuer after a long silence
ranking 5×4
→
Survivorship in the universe: names trading each year, share still listed in July 2026, and median return
table 10×6
→
See all 2,170 queries →