Insights
A Point-in-Time Backtest of Next-Day Movement Candidates
Alphanume Team · July 20, 2026
Backtest the fixed ticker selections as known on each publication date, and let the return labels enter the evaluation table only after the following session closes.
Alphanume's Next-Day Movers dataset is structured for point-in-time grading. The daily selection publishes before the close, historical membership remains fixed, and return plus absolute_move stay null until the next trading session has completed.
The clean backtest has two tables: a selection ledger containing only date and ticker, and an outcome ledger populated later. Keeping them separate makes it impossible for next-session movement to influence cohort membership, thresholds, or execution rules.
Write the information timeline
Moment | Known information | Permitted use |
|---|---|---|
Before publication | No current shortlist | Prior-date research only |
3:30 PM New York | Five current date and ticker selections | Freeze candidate cohort |
Regular close | Same-session prices complete | Apply predefined execution convention |
Following session close | Next-session return and absolute move resolvable | Grade the frozen cohort |
The current API response can contain both membership and resolved outcomes because it is serving history. The dates still define when each field became knowable. Mask the outcome columns in any simulation step that runs before the following close.
Pull a fixed historical window
Use GET /v1/next-day-movers with exact date ranges. Store the raw response, extract the membership ledger, and validate five unique tickers per normal selection date before looking at any result.
import os
import requests
import pandas as pd
response = requests.get(
"https://api.alphanume.com/v1/next-day-movers",
headers={"X-API-Key": os.environ["ALPHANUME_API_KEY"]},
params={"date_gte": "2024-01-01", "date_lte": "2025-12-31"},
timeout=30,
)
response.raise_for_status()
raw = pd.DataFrame(response.json()["data"])
membership = raw[["date", "ticker"]].drop_duplicates().copy()
labels = raw[["date", "ticker", "return", "absolute_move"]].copy()
daily_count = membership.groupby("date")["ticker"].nunique()
assert daily_count.le(5).all()The assertion permits incomplete dates so they can be audited rather than crashing the import. Build a report for every date below five and decide whether it remains in the study. Never fill a missing fifth selection with the next-best realized mover.
Delay the outcome join
Assign each selection an outcome_available_after timestamp equal to the following market close. The backtest state machine can join the label only when simulated time passes that boundary. This makes the same code suitable for historical replay and a live shadow run.
research_contract = {
"selection_available_at": "15:30 America/New_York",
"outcome_available_after": "following session close",
"target": "absolute_move",
"directional_target": False,
"normal_daily_selection_count": 5,
"null_outcome_policy": "unresolved, never zero",
}
assert research_contract["target"] == "absolute_move"Use the served absolute_move as the model-grading label. If the project reconstructs returns from another price source, state that outcome definition separately and compare it with the served label. Differences can come from price timing, adjustments, missing bars, or vendor conventions.
Freeze every configuration value beside the membership ledger: sample dates, eligible control source, movement cutoff, missing-date policy, and statistical interval. Split development and evaluation by calendar time rather than randomly mixing observations from the same market regimes. The five selections on a date are not independent, so resample or aggregate by date when estimating uncertainty. A ticker that appears repeatedly can also concentrate exposure; publish unique-ticker counts and the largest per-name frequency before interpreting a narrow confidence interval.
Separate grading from trading
Study | Required data | Claim |
|---|---|---|
Selection grade | Published candidates and resolved movement labels | Candidates moved more or less under a scorecard |
Underlying strategy | Timestamped equity entry and exit prices | Tradable equity return under stated fills |
Options strategy | Historical chains, quotes, greeks, and costs | Contract-level P&L under stated rules |
A large absolute move can still lose money for an option trade when implied volatility, direction, strike choice, spread, or decay is unfavorable. The endpoint supplies no contract prices or execution data. Keep those simulations outside the selection scorecard.
Audit point-in-time failures
- Outcome leakage. Do not use return or absolute_move to select or filter candidates.
- Clock leakage. A 3:30 PM publication cannot receive an earlier same-day fill.
- Null handling. The newest unresolved outcome is not a zero return.
- Universe substitution. Today's optionable list cannot replace the historical candidate ledger.
- Control hindsight. Matched controls need contemporaneous fields and fixed matching rules.
Historical selections remain fixed, which addresses cohort hindsight for the published list. It does not solve price survivorship, corporate actions, halted outcomes, or transaction costs in external data. Free access supplies a trailing 20-trading-session window ending one session behind the latest observation.
Run a replay before a backtest
Use the Next-Day Movers guide to replay 20 consecutive selection dates. At each simulated 3:30 PM, expose only date and ticker. After the following close, attach labels and append that day's scorecard without changing prior configuration.
When the replay passes, extend the period and compare median absolute movement with a frozen control. Publish missing selection dates and unresolved outcomes before the performance table, then compare the result with the proof page under the exact differences in sample and methodology.