rafhq · omen · playbook

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.

00Build a race fixture

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.

01Pre-register

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).

02Link to a Kalshi market

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.

03Ingest new polls

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.

04Score an event

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.

05Update the live forecast

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.

06Resolve after the election

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.

07Let cron do it

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:

  1. fetch-wikipedia-polls → merges new polls into live-polls
  2. re-runs the hybrid forecast + uncertainty + trajectory (currentForecast)
  3. for elections past, fetch-wikipedia-outcome auto-resolves
  4. score-predictions writes resolution + drift
  5. 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.

08Keep the record honest

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.