Alphanume

Insights

How to Backtest a Stock Universe That Changes Over Time

Alphanume Team · August 1, 2026

Construct the investable universe independently for every date, using monthly optionability snapshots as intervals and versioned classification snapshots when sector labels matter.

A changing-universe backtest fails when it starts from today's ticker list and projects that list backward. New listings receive imaginary history, acquired or delisted names disappear, and stocks can appear optionable before exchanges listed a chain. Alphanume's Historical Optionable Tickers dataset supplies frozen monthly optionability snapshots for this reconstruction.

Each snapshot is taken on the first trading day of the month and records ticker, the four-gap average across nearby expirations, and a flag for whether that average is under nine days. It identifies listed-chain availability and weekly-style density at that snapshot. It does not provide strikes, quotes, greeks, open interest, volume, or executable liquidity.

Represent membership as intervals

Input

Start

End

Meaning

Optionable snapshot row

Snapshot date

Next snapshot date

Chain observed at monthly snapshot

First appearance

First snapshot containing ticker

Later gap or final covered snapshot

Observed entry into dataset

Density flag

Snapshot with has_weeklies equal to 1

Next snapshot

Four-gap average under nine days at that observation

Classification snapshot

Saved retrieval timestamp

Next saved version

Label known to the research archive

Monthly intervals are a research convention, not exact listing timestamps. A chain added or removed mid-month is invisible until the next snapshot. Choose whether the first snapshot governs the whole month or only dates after the snapshot, document that rule, and apply it consistently.

Retrieve every monthly snapshot

The endpoint is GET /v1/optionable-tickers. Query a fixed history and paginate with cursor_date plus cursor_ticker. Results are ordered by date descending and ticker descending.

import os
import requests

url = "https://api.alphanume.com/v1/optionable-tickers"
headers = {"X-API-Key": os.environ["ALPHANUME_API_KEY"]}
params = {"date_gte": "2024-01-01", "date_lte": "2026-06-30"}
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"]

Validate that each date is the expected first trading day and that each ticker appears once per snapshot. Save the raw pages before interval construction. A partial page can look like genuine universe shrinkage if pagination is not audited.

Expand snapshots without future leakage

For each signal date, first select the latest global optionable snapshot on or before that date. Then test whether the ticker exists in that exact snapshot. A per-ticker backward join is wrong because it can carry January membership into February even when the ticker is absent from the newer February snapshot.

snapshot_calendar = (
    snapshots[["date"]]
      .drop_duplicates()
      .rename(columns={"date": "snapshot_date"})
      .sort_values("snapshot_date")
)

signals = signals.sort_values(["signal_date", "ticker"])
signals = pd.merge_asof(
    signals,
    snapshot_calendar,
    left_on="signal_date",
    right_on="snapshot_date",
    direction="backward",
    tolerance=pd.Timedelta("40D"),
)

membership = snapshots.rename(columns={"date": "snapshot_date"})
eligible = signals.merge(
    membership,
    on=["snapshot_date", "ticker"],
    how="left",
    validate="many_to_one",
    indicator="membership_join",
)

eligible["is_optionable"] = eligible["membership_join"].eq("both")
eligible["supports_weeklies"] = eligible["has_weeklies"].eq(1)

A 40-day tolerance covers normal monthly spacing and should still be paired with a missing-snapshot audit. For a weekly-style density rule, require has_weeklies=1; the field means the four-gap average is under nine days. The avg_days_between field describes the nearby expiration ladder at snapshot time and does not prove a specific expiration.

Version classifications separately

Entry, exit, and optionability are dated in the monthly dataset. Ticker Classification is different: its endpoint has no date field and returns the current Alphanume sector and industry mapping. A historical test cannot call it today and treat the result as the label known years ago.

Classification source

Valid historical use

Label in output

Saved dated snapshots

Join latest saved version on or before signal date

Point-in-time under snapshot cadence

Current endpoint response

Current-state attribution only

Current Alphanume classification

Licensed historical taxonomy

Use according to provider contract

Provider-specific label

If no versioned classification archive exists, omit historical sector controls or describe them honestly as current-state groupings. Do not backfill the current label and call it historical. Also call the taxonomy Alphanume's research classification rather than GICS, SIC, or another provider's system.

Keep membership and execution distinct
  • Monthly blind spots. Mid-month chain additions and removals are not observed.
  • Listed versus liquid. A chain can exist with unusable spreads or depth.
  • Expiration detail. The endpoint has no strikes or exact chain inventory.
  • Ticker continuity. Renames, mergers, and reused symbols need a separate identity map.
  • Delisted outcomes. A missing later price should remain in the denominator and receive explicit treatment.

Free access covers the trailing 20 trading sessions after a one-trading-session delay, which can contain only one monthly optionability snapshot. Historical entry and exit inference requires deeper snapshot coverage.

Rebuild one month before scaling

Use the Historical Optionable Tickers guide to rebuild one month's universe. Export the raw snapshot, prior and next snapshot dates, inferred membership intervals, weekly subset, missing signal joins, and any saved classification version used.

Compare that universe with the current optionable list and list every added or removed candidate before calculating returns. Then repeat month by month, freezing membership before joining signals and outcomes. This makes the changing choice set visible instead of letting it disappear inside portfolio statistics.