How to Detect Lookahead Bias in a Backtest: A Practical Checklist
Lookahead bias in a fundamentals backtest almost never announces itself. It shows up as a backtest that looks unusually good, and then a live strategy that quietly stops working. The mechanism is boring: your join used a date the data wasn't actually public yet. This article is a checklist for finding that, not a pitch for a particular dataset — though we'll use our own free sample to show working code, because it's the one we can show you column-by-column without asking anything of you.
This is not investment advice. Nothing here promises a better-performing backtest — only a more honest one, with fewer joins that leak future information into the past.
Symptom 1: Your signal times earnings suspiciously well
If a fundamentals-driven signal enters positions right before earnings-driven price moves with better-than-random timing, check what date you used to decide the fundamental was "known." A common bug: joining on fiscal period end date (e.g., Q4 2019 = 2019-12-31) instead of the date the 10-K was actually filed and made public. Every company reports its fiscal year end weeks to months before the filing exists. If your backtest thinks Q4 data is available on the period end date, it's trading on the future.
Symptom 2: Live performance decays right after paper trading ends
This is the classic tell. A strategy backtests clean, goes live (or into a genuinely walk-forward paper test), and the edge shrinks or disappears. If the strategy relies on fundamentals and nothing else changed — no regime shift, no capacity issue — lookahead bias in the historical joins is the first thing to rule out, because live trading is the one environment where lookahead is structurally impossible.
Symptom 3: Performance is fragile to a few outlier dates
Run your equity curve breakdown by trade. If a disproportionate share of PnL comes from trades placed in a narrow window around known reporting seasons, and those specific trades have unusually strong hit rates, look at whether the fundamental value used in the join was restated later and your backtest is silently using the restated number instead of what was originally reported.
The Join-Date Audit
The fastest way to check: pull your actual join logic and ask, for every fundamental value, "what date did my code think this became true?" Then compare that to the date it was actually filed. Two dates get confused constantly:
- Fiscal period end — when the reporting period closed (always in the past relative to filing)
- First filed date — when the 10-K (or 10-K/A) hit EDGAR and became public
If your pipeline uses the former where it should use the latter, you have a lookahead bug baked into every single row, sized to the actual filing lag for that company and quarter.
A pandas check using first_filed
Our free PIT sample (40 companies, 3,212 rows, no signup) includes a first_filed column specifically so you can run this check yourself. Example:
import pandas as pd
df = pd.read_csv("sample.csv", parse_dates=["first_filed", "fiscal_period_end"])
def check_leakage(df, as_of_date, join_col="fiscal_period_end"):
as_of = pd.Timestamp(as_of_date)
naive_join = df[df[join_col] <= as_of] # what a naive backtest would include
correct_join = df[df["first_filed"] <= as_of] # what was actually public by as_of
leaked = naive_join[~naive_join.index.isin(correct_join.index)]
return leaked
leaked_rows = check_leakage(df, "2020-03-31")
print(f"{len(leaked_rows)} of {len(df)} rows would be lookahead-leaked "
f"if you joined on fiscal_period_end instead of first_filed")
If leaked_rows is non-empty for a realistic as_of date, your naive join is including fundamentals before they existed publicly. Swap the join key to first_filed (or whatever your data vendor's equivalent "became public" field is called) and rerun.
The Lag-Shift Test
A second, independent check that doesn't require inspecting your join code line by line: shift your as_of date backward by a fixed number of days across your entire backtest — say 30, 60, 90 days — and rerun. If performance is fairly stable under small shifts, that's a decent sign your logic isn't sitting right on a knife's edge of leaked information. If performance craters the moment you shift even slightly earlier, your original result was likely dependent on data being available earlier than it should have been. This is a blunt instrument, not proof, but it's cheap to run and catches a lot of accidental lookahead.
For deeper background on why this matters mechanically, see lookahead bias in fundamental backtests and our overview of point-in-time fundamentals.
Comparing Your Options for Fixing It
| Approach | Effort | Coverage | Cost | Best for |
|---|---|---|---|---|
| Scrape EDGAR yourself, track filing dates | High | Whatever you build | Your time | Teams needing full control, custom concepts, or non-US data |
| Research-grade PIT vendors (e.g. Sharadar, Tiingo, QuantConnect) | Low | Broad, often quarterly + more concepts | See their pricing pages | Funds/teams needing quarterly data, longer history, or bulk delivery — this is genuinely where they win over us |
| Tradevo Data | Low | 5,212 US companies, 7 concepts, annual only, up to 12 fiscal years | $49/mo | Individual quants who need PIT-safe annual US fundamentals cheaply, via API |
| Free PIT sample (GitHub) | Low | 40 companies, 3,212 rows | Free | Prototyping the join logic and running the checks in this article before paying for anything |
We're not the only affordable option, and we're not claiming to be — the vendors above are credible and some cover more ground (quarterly data, longer history, bulk delivery). See their pricing pages directly since we won't quote numbers we don't control.
When to Build It Yourself (or Use Someone Else)
Being honest about where Tradevo Data isn't the right tool — and where the vendors above genuinely win:
- You need quarterly fundamentals. We're annual-only (10-K + 10-K/A). Quarterly PIT logic is a roadmap item, not shipped. A vendor with quarterly coverage wins here.
- You need non-US equities. We're US-only, sourced from SEC EDGAR. A vendor with international coverage wins here.
- You need more than 7 concepts, or line items beyond Revenue, NetIncome, Assets, StockholdersEquity, OperatingCashFlow, EPSDiluted, and DilutedShares.
- You need Parquet files specifically — bulk is included (a one-call gzipped-CSV download of the full dataset via
/v1/download, plus a whole-universe/v1/snapshot), but the delivery format is CSV/JSON today; Parquet is on the roadmap. - You're already comfortable parsing EDGAR filings directly and tracking
first_filedyourself — that's a legitimate, free path if you have the engineering time, and it's exactly what our free sample's methodology documents.
If none of those apply and you just want a point-in-time-safe first_filed/original_value pair for US annual fundamentals without building the EDGAR pipeline yourself, that's the actual use case we built for.
Try It Before You Pay for Anything
Start with the free sample — 40 companies, full methodology, no signup — and run the join-date audit and lag-shift test above on your own logic. If it holds up and you want the full 5,212-company, 313,489-row dataset with first_filed, original_value, latest_value, and restatement flags via API, it's $49/mo at Tradevo Data, instant key after checkout, cancel anytime, docs at /docs.
If you're actively comparing PIT data providers, our alternatives page lays out where we fit and where we don't.
Not investment advice; verify competitor pricing yourself on their own pricing pages.