Valuein
quant
beginner
8 min

Avoiding Look-Ahead Bias: A Case Study

A worked example showing why look-ahead bias inflates backtest returns. Compare 'as-reported' vs 'restated' values on a known restatement and quantify the alpha leak.

Python SDKDuckDB

The hidden tax on quant research

Most published quant research silently uses restated financials — values that were updated months or years after the original filing. Backtests over the same period using as-reported data routinely underperform their academic counterparts by 100-300 bps annually. The mechanism is simple: restatements often correct accounting errors that flagged real problems, so a strategy that 'knew' about the correction in advance avoids the eventual drawdown.

The pattern: query as-reported vs restated

Every fact in Valuein's warehouse carries an accepted_at timestamp. To pull the value as it was originally reported, take the earliest accepted_at per period; to pull the latest restated value, take the most recent. Two conventions keep the query correct: fact keys on entity_id (the zero-padded CIK — resolve a ticker via the references table), and periods are grouped by period_end, never fact.fiscal_year (comparative figures in later 10-Ks share a period_end but carry the later filing's fiscal_year tag). The query below shows both values side-by-side for GE.

python
from valuein_sdk import ValueinClient GE_CIK = "0000040545"  # fact keys on entity_id; resolve tickers via `references` with ValueinClient() as client:    df = client.run_query(f"""      WITH original AS (        SELECT entity_id, period_end, numeric_value AS as_reported        FROM fact        WHERE entity_id = '{GE_CIK}'          AND standard_concept = 'NetIncome'          AND fiscal_period = 'FY'          AND period_end BETWEEN DATE '2015-01-01' AND DATE '2018-12-31'        QUALIFY ROW_NUMBER() OVER (          PARTITION BY entity_id, period_end          ORDER BY accepted_at ASC        ) = 1      ),      latest AS (        SELECT entity_id, period_end, numeric_value AS restated        FROM fact        WHERE entity_id = '{GE_CIK}'          AND standard_concept = 'NetIncome'          AND fiscal_period = 'FY'          AND period_end BETWEEN DATE '2015-01-01' AND DATE '2018-12-31'        QUALIFY ROW_NUMBER() OVER (          PARTITION BY entity_id, period_end          ORDER BY accepted_at DESC        ) = 1      )      SELECT        year(o.period_end) AS fiscal_year,        o.as_reported / 1e9 AS as_reported_b,        l.restated / 1e9 AS restated_b,        (l.restated - o.as_reported) / 1e9 AS revision_b      FROM original o      JOIN latest l USING (entity_id, period_end)      ORDER BY fiscal_year    """)    print(df)

Why this matters: a worked example

GE restated 2017 financials in early 2018 after the Power segment writedowns. A naive value screen run today using restated data would correctly mark GE as troubled in 2017. The same screen run with PIT data — only seeing the as-reported numbers available on Dec 31, 2017 — would have rated GE attractively. The stock then dropped 50% in 2018. Models that 'cheated' by using restated data avoided this drawdown in backtests; models that used PIT data took the full hit, just like real money would have. That's a 200-500 bps alpha gap entirely attributable to look-ahead bias.

The PIT discipline in one query

Anywhere you run a screen or backtest, filter on accepted_at (or create the SDK client with `as_of`, which does it for every point-in-time table). The pipeline guarantees accepted_at is the SEC acceptance timestamp — pre-restatement, pre-revision. Filtering on it is the single most important defense against look-ahead bias.

python
from valuein_sdk import ValueinClient with ValueinClient() as client:    # Universe + facts as known on 2017-12-31    df = client.run_query("""      WITH universe_at AS (        SELECT DISTINCT im.cik        FROM index_membership im        WHERE im.index_name = 'SP500'          AND im.effective_date <= DATE '2017-12-31'          AND (im.removal_date IS NULL OR im.removal_date > DATE '2017-12-31')      ),      known AS (        -- Latest vintage accepted by the cutoff, per company and concept        SELECT entity_id, standard_concept, numeric_value        FROM fact        WHERE accepted_at <= TIMESTAMP '2017-12-31 23:59:59'          AND fiscal_period = 'FY'          AND period_end BETWEEN DATE '2017-01-01' AND DATE '2017-12-31'          AND standard_concept IN ('NetIncome', 'TotalRevenue')        QUALIFY ROW_NUMBER() OVER (          PARTITION BY entity_id, standard_concept ORDER BY accepted_at DESC        ) = 1      ),      known_metrics AS (        SELECT          entity_id,          MAX(CASE WHEN standard_concept = 'NetIncome'    THEN numeric_value END) AS net_income,          MAX(CASE WHEN standard_concept = 'TotalRevenue' THEN numeric_value END) AS revenue        FROM known        GROUP BY entity_id      )      SELECT u.cik, k.revenue, k.net_income,             k.net_income / NULLIF(k.revenue, 0) AS net_margin      FROM universe_at u      JOIN known_metrics k ON k.entity_id = u.cik      ORDER BY net_margin DESC NULLS LAST      LIMIT 50    """)    print(df)

Where to go next

Apply the same pattern to any backtest. The Point-in-Time Backtesting guide shows the full monthly-rebalance loop. The Methodology page documents how accepted_at differs from filing_date and report_date — and why it's the only timestamp safe to use for PIT analysis.

Up next

SEC Financial Statement Data Sets: What They Are and Their Limits

The SEC's quarterly Financial Statement Data Sets (num, sub, tag, pre) explained: what each file holds, how to join them by accession number, and where they fall short for backtests and screens.

Read next

Run these examples on real filings

Start with the free sample, no token and no card.

Start free