Blog

Look-ahead bias in backtesting, and how point-in-time data prevents it

· 7 min read · Backtesting, Data

A backtest asks one question: if I had run these rules in the past, what would have happened? It can only answer honestly if, at every moment of the simulation, the strategy sees exactly what it could have seen then, and nothing more. When it sees more, that's look-ahead bias, and it is the most common reason a strategy that looks excellent on paper loses money live.

The trouble is that look-ahead bias rarely comes from an obvious mistake like reading tomorrow's price. It hides in the data itself, in when values became public and in what they said at the time.

Where look-ahead bias hides

1. Reporting delays

A company's quarter ends on 31 March, but its 10-Q isn't filed with the SEC until weeks later: large companies have 40 days, smaller ones 45, and annual reports take longer still. A dataset indexed by the quarter's end date makes the figures look available on 31 March. A strategy that trades on them then is trading on numbers nobody had.

2. Revisions

Many economic series are revised after their first release. US GDP comes out as an advance estimate about a month after the quarter, then a second and a third estimate, then annual revisions years later. Employment and inflation data are revised too, and companies restate their accounts. A table of today's values gives the backtest the corrected number, not the one the market reacted to on the day.

3. Release times within the day

Even the date is not enough. The CFTC's Commitments of Traders report describes positions as of Tuesday but is published on Friday afternoon. Daily reference rates come out at a set time of day. If the strategy acts on a value at the open of a day that it only became public at the close of, the backtest has still looked ahead, by a few hours.

4. Fills at a price that already passed

An order decided on a bar's close can't fill at that same close: by the time you know the close, it's gone. A realistic engine fills market orders on the next price after the decision. Spreadsheet backtests that buy “at today's close” on a signal computed from today's close are a classic source of returns that can't be traded.

5. Survivorship

A universe built from today's index members, or today's listed stocks, leaves out the companies that were delisted, acquired or went bankrupt. Those are disproportionately the losers, so a strategy tested only on survivors looks better than it would have been. This is survivorship bias, a close cousin of look-ahead bias: the backtest knows who survived.

What point-in-time data means

Point-in-time data stores each value with the moment it became public, and the value as it was published then. Replayed in a backtest, every row arrives at its release time, not its reference date, and a later revision arrives later as a new row instead of overwriting history.

On MarketsCoder, every dataset works this way. For example:

  • US company fundamentals arrive at 17:30 New York time on the day each 10-K or 10-Q was filed, not on the period end.
  • US macro indicators carry the value as first published and every revision after, each on the date it came out.
  • Commitments of Traders reports arrive at the CFTC's actual release time, including when a holiday delays it.

Because the data arrives on time, the strategy code stays simple: you react to a row when it shows up, and the engine guarantees you couldn't have seen it earlier.

from AlgorithmImports import *
from lean_platform_data import SecFundamentals


class FundamentalsAlgorithm(QCAlgorithm):
    def initialize(self):
        self.set_start_date(2014, 1, 1)
        self.set_end_date(2021, 1, 1)
        self.aapl = self.add_equity("AAPL", Resolution.DAILY).symbol
        # Rows arrive at 17:30 New York time on the filing date.
        self.fundamentals = self.add_data(SecFundamentals, "AAPL").symbol

    def on_data(self, slice):
        if self.fundamentals in slice:
            f = slice[self.fundamentals]
            if f.net_income and f.revenue and f.net_income / f.revenue > 0.2:
                self.set_holdings(self.aapl, 1)
            else:
                self.liquidate(self.aapl)

A checklist before you trust a backtest

  • Does every input have a publication time, not just a reference date?
  • Does the strategy see values as first published, or today's revised ones?
  • Do orders fill after the decision, at a price that was still available?
  • Does the universe include assets that later disappeared?
  • Are fees, spreads and slippage included?

If any answer is “no” or “not sure”, treat the result as an upper bound, not an estimate. And even with clean data, a strategy can still fit the past too closely: that's overfitting, a different problem with different defences.

Test your own idea on point-in-time data.

Write a strategy in Python or describe it to the AI assistant, and see a full backtest report in minutes.

Get started