Alphanume

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.