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.
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.
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.
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.