Insights
Avoiding Lookahead Bias in Market-Cap Backtests
Alphanume Team · August 1, 2026
Build every size bucket from the market-cap and share-count row dated to that rebalance, never from a current company profile copied across the historical sample.
A market-cap backtest develops lookahead bias when it ranks old signals using today's shares outstanding or current capitalization. That shortcut moves companies between size buckets using information learned later and can remove firms that disappeared before the present. Alphanume's Historical Market Cap dataset supplies dated market cap and shares outstanding for the reconstruction.
The correct unit is the observation available for the date being tested. Keep the market-cap date beside every signal row, reject future matches, and freeze the eligible universe before calculating returns. Dated capitalization repairs one important leak while leaving listing history, corporate actions, price adjustments, and missing securities as separate responsibilities.
See where the leak enters
Backtest step | Leaky input | Time-safe input |
|---|---|---|
Size screen | Current market cap | Market cap on or before the signal date |
Share denominator | Latest company-profile shares | Dated shares outstanding row |
Universe | Tickers trading today | Contemporaneous listing or event universe |
Outcome | Only securities with clean later prices | All eligible rows plus explicit missing outcomes |
The common failure is subtle because the code can still join perfectly on ticker. A 2022 signal matched to a 2026 company profile has no missing keys, yet the size field contains four years of future financing, buybacks, mergers, and price movement.
Request the rebalance date
The endpoint is GET /v1/historical-market-cap. It returns date, ticker, market_cap, and shares_outstanding. For a market-wide rebalance, use one exact date or a narrow date window and paginate with both cursor fields.
import os
import requests
url = "https://api.alphanume.com/v1/historical-market-cap"
headers = {"X-API-Key": os.environ["ALPHANUME_API_KEY"]}
params = {"date": "2026-02-06"}
rows = []
while True:
response = requests.get(url, headers=headers, params=params, timeout=30)
response.raise_for_status()
payload = response.json()
rows.extend(payload["data"])
if not payload["has_more"]:
break
params["cursor_date"] = payload["next_cursor"]["date"]
params["cursor_ticker"] = payload["next_cursor"]["ticker"]Responses are ordered by date descending and ticker descending. A request that supplies only cursor_date or only cursor_ticker is invalid. Save every page and confirm the returned dates match the requested rebalance before building buckets.
Contrast dated and current shares
Run the same signal sample twice. Version A joins the dated Alphanume row for each rebalance. Version B deliberately applies one current share-count file across every date. Compare bucket membership before comparing returns, because the membership difference is the direct footprint of the leak.
Audit output | Calculation | Why it matters |
|---|---|---|
Bucket migration | Current bucket minus dated bucket | Shows future size information changing selection |
Share-count gap | Current shares divided by dated shares minus one | Surfaces later issuance or buybacks |
Coverage gap | Signals without a dated match | Prevents silent survivor filtering |
Return difference | Time-safe result minus leaky result | Measures the practical impact after membership is frozen |
Do not interpret every share-count difference as dilution or buybacks without a filing-level source. Splits, source revisions, mergers, and coverage changes can affect the series. The audit identifies where current fundamentals alter history, then routes large differences into evidence review.
Use an as-of join when dates differ
If signals arrive on dates with no market-cap row, match the latest observation on or before the decision date under a maximum staleness rule. Never select the nearest row in both directions because that can choose tomorrow's value. Store cap_date, signal_time, and the lag for every match.
signals = signals.sort_values(["signal_date", "ticker"])
caps = caps.sort_values(["date", "ticker"])
panel = pd.merge_asof(
signals,
caps,
left_on="signal_date",
right_on="date",
by="ticker",
direction="backward",
tolerance=pd.Timedelta("5D"),
)
matched = panel["date"].notna()
assert (panel.loc[matched, "date"] <= panel.loc[matched, "signal_date"]).all()A five-calendar-day tolerance is an example, not a universal rule. Choose it from the strategy clock and data cadence. Export stale and unmatched rows rather than converting missing capitalization to zero or dropping them before the denominator is reported.
Know what market cap cannot repair
- Survivorship. Dated fields do not create a complete historical listing universe by themselves.
- Corporate actions. Split, merger, distribution, and ticker-change handling still belongs in the price pipeline.
- Backfills and revisions. Archive the served snapshot if exact run-time reproducibility matters.
- Tradability. Market cap does not measure spread, borrow, halted status, or capacity.
- Missing coverage. Unmatched securities can be economically different from cleanly covered survivors.
Free access provides the trailing 20 trading sessions after a one-trading-session delay. That is enough to test the join mechanics. A long backtest needs the full history plus an independent historical universe and outcome policy.
Publish the membership audit first
Follow the Historical Market Cap guide, choose one rebalance date, and export the raw response, bucket thresholds, dated membership, current-fundamental comparison, stale matches, and missing rows. Verify ten large bucket migrations against source history before calculating any strategy return.
Then rerun the full sample with buckets rebuilt independently at every rebalance. Report how many positions changed, how the size distribution moved, and how the result changed after all missing outcomes remained visible. That audit shows exactly what the current-data shortcut had contributed.