How to run a prediction.
The full operational loop. Every step is a CLI command; the same scripts power the public pages. If the race you want to forecast isn't on a fixture yet, start at Step 0.
Open dashboard/src/lib/omen/politics/fixtures.ts and add a new RaceInfo + events block. Minimum required fields:
- raceId, name, type, candidates, electionDate, district
- pregamePrior (sums to ~1)
- demographics.partisanLean (−1..+1)
- voterMix (PRIMARY_MIX / GENERAL_MIX / RUNOFF_MIX / OFF_YEAR_MIX)
- candidateIdeology (for proximity routing; especially important for primaries)
- At least an election_day event
For ACS populations: pick the state FIPS + PUMA codes covering the district, then:
tsx scripts/fetch-acs-pums.ts <cacheKey> <stateFips> <puma1,puma2,...> # e.g. nc-senate-2026: tsx scripts/fetch-acs-pums.ts nc-statewide 37 ""
Wire acsCacheKey into your fixture. For statewide races pass an empty PUMA list to pull the whole state.
At least 72 hours before election day:
tsx scripts/preregister-prediction.ts <fixtureKey>
Refuses automatically if electionDate is closer than 72 hours. Commits the forecast to data/predictions/<id>.json — immutable. This file is your public commitment; git history + git commit hash stamp the moment of pre-reg.
Optional flags: --trials 200 (Monte Carlo count, default 100), --voters 5000 (population size).
If a Kalshi market exists for this race, add a kalshi block to the prediction JSON:
{
"kalshi": {
"yesMarketTicker": "KXSEN2026NC-MCCRORY-NOV03",
"yesCandidate": "mccrory"
}
}Then check your edge vs market pricing:
tsx scripts/compare-to-kalshi.ts <predictionId>
Prints OMEN P(win) − market-implied P(yes) in pp. Positive → OMEN more bullish than market.
Every time a new poll drops, either paste it in manually:
tsx scripts/add-poll.ts <raceId> \ --pollster "NYT/Siena" --fieldEnd 2026-10-15 \ --n 850 --pop lv \ --share candidateA=48.2 --share candidateB=43.1
Or bulk-scrape a Wikipedia poll aggregator table (LLM extraction, needs ANTHROPIC_API_KEY):
tsx scripts/fetch-wikipedia-polls.ts <raceId> \ https://en.wikipedia.org/wiki/2026_X_election \ --since 2026-09-01
Both write to data/live-polls/<raceId>.json. Existing fixture polls aren't touched.
When a major news event happens (debate moment, scandal, major endorsement), score it via the rubric:
tsx scripts/score-event.ts <fixtureKey> 2026-10-05 \ "Harris-Trump debate: Harris gets under Trump's skin, strong post-debate polls"
Paste the printed event JSON into data/live-events/<raceId>.json. Next forecast update will include it.
Daily (or after any feed ingest):
tsx scripts/update-forecast.ts <predictionId> # or --all to update every pending prediction
Re-runs hybrid forecast + Monte Carlo + trajectory using merged fixture + live feeds. Writes currentForecast onto the prediction with drift pp vs the frozen pre-reg number. Pre-reg forecast stays untouched.
/omen/predictions cards render the drift strip automatically.
When results are in, either fetch from Wikipedia:
tsx scripts/fetch-wikipedia-outcome.ts <predictionId> \ https://en.wikipedia.org/wiki/2026_X_election
Or enter manually:
tsx scripts/resolve-prediction.ts <predictionId> \ --winner X --share X=52.4 --share Y=45.1 --turnout 61
Both write to data/outcomes/<predictionId>.json. Then:
tsx scripts/score-predictions.ts
Fills the prediction's resolution block with actual outcome + score (share err / margin err / Brier / beatPregame). Green-or-red strip appears on /omen/predictions.
Every pending prediction with a wikipediaUrl gets auto-updated nightly by .github/workflows/omen-cron.yml (14:00 UTC). This is now the legacy lab workflow:
- fetch-wikipedia-polls → merges new polls into live-polls
- re-runs the hybrid forecast + uncertainty + trajectory (currentForecast)
- for elections past, fetch-wikipedia-outcome auto-resolves
- score-predictions writes resolution + drift
- commits the diff back to main
To run locally: tsx scripts/omen-cron.ts [--dry-run] [--skip-polls]. Anthropic key required (used by Wikipedia fetchers). vNext campaigns should move away from this path before they become watchlist or tradable.
Run periodically:
tsx scripts/run-omen-pol-backtest.ts --hybrid --emit-stats
Regenerates src/app/omen/backtest-stats.ts and updates the honest T-30 numbers on /omen. Tests run alongside:
tsx scripts/test-omen.ts
Any regression in the 57-test suite fails fast. The calibration reliability panel on /omen/predictions updates automatically as resolutions accumulate.