Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Look-Ahead Bias: The Backtest Killer

Look-ahead bias is future data leaking into a backtest. See how same bar decisions, survivorship, and revised figures inflate results, and how to test for it.

Look-ahead bias is the use of information inside a backtest that nobody could have held at the moment the simulated trade was placed. The equity curve climbs, the statistics look excellent, and none of it survives contact with a live order. This page defines the bias, walks three mechanisms that produce it, and measures each one on real market data.

What look-ahead bias is

A backtest is a claim about a decision: at this timestamp, with this information, the rule would have done this. The claim holds only when every input the rule reads already existed, in its final form, before that timestamp. Look-ahead bias is any breach of that condition, and it is silent. Nothing in a spreadsheet or a script complains when a row hands back a value stamped later than the decision it feeds.

Three breaches cover most of what turns up in practice. A decision that reads a price bar it also trades inside. A universe assembled from the names that are still listed today. A data field that was revised after the date it carries.

Mechanism one: deciding on a bar you also trade

Start with a rule that sounds harmless. Buy on strength, meaning on sessions where the fund finishes above where it opened. Coded carelessly, the test enters at that session's open and exits at that session's close, while the condition it screens on is the closing price of the same bar. Nobody knows a closing price at the opening bell.

The panel below runs both versions on SPY, the S&P 500 tracker, across ten calendar years. The first column is the impossible one. The second lags the identical condition by a single session, which is the earliest a funded account could act on it.

QuerySame-bar decision vs a one-session lag: SPY, average session gain, 2016-2025
The exact SQL behind every number
WITH bars AS (
    SELECT date,
           toFloat64(open) AS o,
           toFloat64(close) AS c,
           toFloat64(close) > toFloat64(open) AS up_day
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= toDate('2015-12-01')
      AND date <= toDate('2025-12-31')
),
lagged AS (
    SELECT date, o, c, up_day,
           any(up_day) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prior_up
    FROM bars
)
SELECT toYear(date) AS year,
       countIf(up_day) AS signal_days,
       round(100 * avgIf(c / o - 1, up_day), 3) AS same_bar_avg_pct,
       round(100 * avgIf(c / o - 1, prior_up), 3) AS lagged_avg_pct,
       round(100 * (avgIf(c / o - 1, up_day) - avgIf(c / o - 1, prior_up)), 3) AS gap_pp
FROM lagged
WHERE date >= toDate('2016-01-01')
GROUP BY year
ORDER BY year
Run this yourself

The separation is not subtle. In 2016 the same-bar version averaged 0.447% per qualifying session while the lagged version averaged -0.008%, a spread of 0.455 percentage points a day. In 2025 the two read 0.636% and -0.03%. With roughly 140 qualifying sessions in a year, a spread of half a percentage point a session compounds into an equity curve no live account has ever printed.

The whole edge is definitional. Selecting sessions that closed above their open, then measuring open-to-close on those same sessions, measures a quantity guaranteed positive. The rule discovered its own filter.

Variants of this hide in daily data all the time: a moving average that includes today's close and is used to trade today, a signal computed on prices already adjusted for a split that had not yet happened, an intraday stop placed at a session high that had not yet printed. The common shape is a decision timestamp sitting earlier than one of its own inputs.

Mechanism two: a universe drawn from today's survivors

The second mechanism never touches the rule. It sits in the list of names the rule is allowed to see.

Pull a ticker list from a current data provider, run it back ten years, and the sample quietly excludes every company that went bankrupt, was acquired, or was delisted along the way. The rule was never offered the losers. The panel measures the size of that omission: every US-listed name that traded at least 200 sessions above $1 in a given calendar year, the share of that cohort still printing prices in July 2026, and the median calendar-year return of the whole cohort beside the median of the survivors alone.

QuerySurvivorship in the universe: names trading each year, share still listed in July 2026, and median return
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
Run this yourself

Of the 7063 names that cleared the bar in 2015, 52.5% were still trading in July 2026. For the 2024 cohort the figure is 87.5%, mostly a matter of elapsed time: a 2024 name has had two years to disappear and a 2015 name has had eleven.

The return columns carry the bias itself. In 2017 the median name returned 7.51% while the median survivor returned 11.32%, a gap of 3.81 percentage points handed over for free. The gap is positive in every one of the 10 years charted. A strategy tested on survivors inherits it before making a single decision, and rules that screen for weakness inherit more than the median, since the names that vanished sit disproportionately in that bucket.

The correction is a point-in-time universe: the list of names as it stood on each rebalance date, delistings included, with a delisted position closed at its final print rather than dropped from the record.

Mechanism three: data that was revised after its date stamp

Prices are stamped once. Fundamentals are not. A quarterly figure carries the quarter's end date, gets amended by a later filing, and a database that stores only the current version will happily serve the amended number against the original date. A rule screening on that number is reading a correction that took months to arrive.

The same pattern runs through index membership lists rebuilt to current constituents, analyst estimate histories overwritten with the final consensus, and corporate action adjustments applied backward across an entire price series. In each case the stored record is a present-tense snapshot wearing a past-tense date.

Two practices contain it. Use a source that keeps vintages, meaning the value as it was known on each date rather than the value as it is known now. Where vintages are unavailable, apply a reporting lag generous enough to cover the real one, and treat the length of that lag as a parameter worth testing rather than a detail worth guessing.

What the curve looks like when the future leaks in

Look-ahead bias has a signature: performance that sits far above what the underlying market offered. A useful reference point is the hindsight ceiling, the growth a perfect one-day-ahead forecast would have produced on a single instrument.

QueryThe hindsight ceiling: SPY buy and hold, the same year without its biggest up days, and perfect one-day foresight
The exact SQL behind every number
WITH bars AS (
    SELECT date,
           toFloat64(close) AS c,
           any(toFloat64(close)) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
      AND date >= toDate('2015-12-01')
      AND date <= toDate('2025-12-31')
),
rets AS (
    SELECT toYear(date) AS year,
           c / prev_c - 1 AS r
    FROM bars
    WHERE date >= toDate('2016-01-01')
      AND prev_c > 0
)
SELECT year,
       count() AS sessions,
       round(100 * (exp(sum(log(1 + r))) - 1), 1) AS buy_and_hold_pct,
       round(100 * (exp(sumIf(log(1 + r), r < 0.02)) - 1), 1) AS without_big_up_days_pct,
       round(exp(sum(log(1 + abs(r)))), 1) AS hindsight_ceiling_multiple
FROM rets
GROUP BY year
ORDER BY year
Run this yourself

The ceiling is enormous. In 2020, perfect one-day foresight on SPY multiplied capital 27.7 times over 253 sessions, against 16.2% for simply holding the fund. In the calmest year of the ten, 2017, the same perfect forecast returned 2.2 times capital.

The middle column shows how concentrated that ceiling is. Strip out the sessions that gained more than 2%, and 2020 turns from 16.2% into -44.9%. A leak worth only a handful of those sessions a year moves a backtest a very long way. A curve with shallow drawdowns, a high hit rate, and its best days clustered on the market's most violent sessions has earned an audit.

Calibration matters more than any single threshold. A rule claiming a smooth 40% a year over a decade is claiming a large fraction of the hindsight ceiling, and the honest question is which input told it what was about to happen.

How to test a backtest for look-ahead bias

  • Lag everything by one bar. Shift every signal one full period later and re-run. Genuine edges decay a little. Leaks collapse, often to nothing.
  • Perturb the future. Take the data after each decision timestamp, replace it with noise or a reshuffle, and re-run the decision logic. Every decision should come out identical. Any decision that moves has read something it could not have known.
  • Treat timestamps as data. For each input, store the moment a value became available beside the moment it describes. A join that never compares those two columns cannot enforce the rule.
  • Rebuild the universe as of the date. Include delisted and acquired names, close them at their last print, and run the same rule on both universes to size the survivorship contribution.
  • Measure against the ceiling. Put the strategy's return beside a buy and hold figure and a perfect-foresight figure for the same window. Distance from the ceiling is context a Sharpe ratio alone does not give.

Position sizing sits downstream of all of this. An inflated win rate feeds an inflated bet size, which is where Kelly criterion position sizing turns an accounting error into a drawdown. The same discipline applies before real capital moves: paper trading before real money tests order mechanics, and automated stacks carry the identical hazard at machine speed, covered in multi agent AI trading systems. Effects that survive careful point-in-time testing, such as the low volatility anomaly, are interesting for exactly that reason.

FAQ

What is look-ahead bias in backtesting?

It is the use of data in a simulated trade that was not available when the trade would have been placed. Common forms are deciding on a price bar the trade also executes inside, screening a universe of companies that are still listed today, and reading a financial figure that was revised after the date attached to it.

How can you detect look-ahead bias?

Lag every signal by one full period and re-run: a leak usually collapses while a real edge decays gently. A stronger test replaces all data after each decision timestamp with noise and confirms that every decision comes out unchanged.

Is survivorship bias the same as look-ahead bias?

They are distinct but they travel together. Survivorship bias is a sample built from the names that lasted, and knowing which names lasted is itself information from the future, so a survivor-only universe is a look-ahead leak located in the data rather than in the rule.

Why do backtests with look-ahead bias look so good?

The leak selects outcomes rather than predicting them. In the panel above, screening sessions on their own closing price produced an average session gain about 0.455 percentage points above the lagged version in 2016, an advantage that is definitional rather than earned.

What is point-in-time data?

Point-in-time data stores each value as it was known on each historical date, including the original figure and every later revision, alongside the universe of securities as it stood that day. It is the reference a backtest needs to show that its inputs existed before its decisions.


Every figure above is a stored, versioned query over real daily bars. Expand any panel's SQL, or run the lag test on your own rule on the Strasmore terminal.

#backtesting#look-ahead bias#survivorship bias#quant#research process