Parity rebuild: every valid combo on par with F1 on Polymarket¶
The plan for rebuilding every backtest and the paper-trading P&L with the F1-on-Polymarket pipeline, for every sport and exchange we cover. Staging first; prod only after the owner reviews staging. It also specifies C0, the read-only coverage counter that comes first. The owner's go, 2026-10-07: C0 only for now.
Status (2026-10-14): C0 is built: racinglines backtest coverage (CLI,
parity sprint PR #81, with C2, C3, C4, C5, C7, C9, C10, C11, C12 and C42), read-only, counts links, races and tapes
per sport × exchange × kind × season over Postgres and the Parquet archive. The counts below are the ones the plan
was written from (a read-only query of the VM database on 2026-10-07 at 04:10 UTC, against main c1b7277); run
C0 on the VM for today's, and see Sports and exchanges for production's link counts on 2026-10-14.
The rest of the packages are tracked on the Roadmap.
Sprint goal (owner, 2026-10-07)¶
When this sprint ends, F1, NASCAR and MotoGP should each support two uses:
- Our model against the prediction markets. Polymarket, Kalshi and OG.com wherever they list the sport: fair prices on the board, backtests and paper P&L for every valid combo. That is the parity work below.
- Expected value (EV) of a sportsbook slip. Price each leg of a generic sportsbook's slip with our model, and show the prediction-market price beside it wherever a linked market exists. Report the EV against both. That is package C9 below.
For C9, how the model and the market price are combined is a decision for the owner. It gets a decision-log entry before any blend is treated as final. Until then, both prices and both EVs are shown side by side.
Road cycling (C6) is outside this sprint.
What "on par" means¶
The F1/Polymarket pipeline has eight stages. A sport × exchange × market kind ("combo") is on par when it passes all eight:
| # | Stage | F1 · Polymarket | Where it lives |
|---|---|---|---|
| 1 | Link: the exchange market is linked to race, athlete and kind by exact keys | yes | market_links (prediction = kind) |
| 2 | Tape: prices, trades and books are recorded and archived | yes, 2025 and 2026 | market_price_history, market_trades, market_book_snapshots, data/archive/markets/<exchange>/ (markets/store.py) |
| 3 | Price as of: each weekend stage is priced with no leakage | yes | pipelines/weekend_sweep.py, position_sim |
| 4 | Settle: from results and from the exchange outcome | yes | markets/kinds.py, private_book.py |
| 5 | Sweep: 4 taker modes and 5 maker variants, tuned on one season and held out on the other | yes | f1 search, pipelines/search.py, sweeps/*.toml |
| 6 | Report: a leaderboard and a verdict for each strategy | yes | search_report.py, Lab Edge Finder, MCP edge_finder |
| 7 | Paper: profile → live paper signals every 5 minutes | yes | pipelines/signals.py, profiles.py |
| 8 | Track record: shown on the web and MCP | yes | pipelines/story.py, MCP track_record |
Valid-combo bar (proposed; owner to confirm):
- parity-2: at least 8 settled races with tape in each of two seasons.
- parity-1: at least 8 settled races with tape in one season. It gets P&L but no held-out verdict.
- thin: 1 to 7 races. Reported only.
- missing: no linked markets.
Combos on 2026-10-07 (linked races, 2025 / 2026)¶
The counts are races that have at least one linked market of the kind. The season comes from the market's
end_date. Tape was not counted (it lives mostly in Parquet), so a combo can drop a tier once C0 counts its tape.
| Sport · exchange | parity-2 | parity-1 | thin | missing |
|---|---|---|---|---|
| F1 · Polymarket | win 22/20, h2h 15/19, pole 17/19, team top 21/20 | podium 3/18 | fastest lap 0/4, sprint 0/1, rain/red flag/safety car ≤ 5 | |
| F1 · Kalshi | win 24/16, podium 22/16 | pole 3/15, top 10 0/16, fastest lap 3/16, team top 0/13 | h2h 0/2, top 5 0/1, mover 0/1 | |
| F1 · OG.com | win 0/1 (season futures otherwise) | |||
| NASCAR · Kalshi | win 8/33 | podium, top 5, top 10, top 20 0/29; fastest lap 0/21; pole 0/12; team win 0/17 | h2h 0/6, mover 0/2 | |
| NASCAR · Polymarket | win 7/12 | |||
| NASCAR · OG.com | race markets (champion only, 17 open) | |||
| MotoGP · Kalshi | win 0/13 | |||
| MotoGP · Polymarket | win 4/0 | |||
| MotoGP · OG.com | everything | |||
| Road cycling · any | everything: no links, events or results in the database |
Totals: 7 parity-2, 14 parity-1, 13 thin. Only the F1 combos have a sweep (244 sweep runs, 40 on Kalshi) and
live paper signals today.
Data problems to fix before trusting non-F1 results:
- Unmodeled links. Every NASCAR and MotoGP link has
prediction = 'unmodeled'and keeps its kind inparams.kind, becausemarkets/kalshi/sync.pysetsmodeled = sport == "f1". - Other series under NASCAR Cup. Kalshi NASCAR has 3,101 race-win links over 33 races in 2026, about 94 per
race. One champion link asks about the Truck Series champion. Inferred: markets from other series are filed
under
nascar_cup. - F1 links left unmodeled. 831 F1 Kalshi links from 2026 are unmodeled, among them "to finish top 5" titles.
- MotoGP champion links have no kind. On both Kalshi and Polymarket,
params.kindis empty.
Every session, every supportable kind (owner, 2026-10-07: "close this gap")¶
The tables above count only the kinds we already link. This section lists everything the exchanges list for F1,
NASCAR and MotoGP that we don't price yet. It comes from the unmodeled links in a read-only query on 2026-10-07,
grouped by Kalshi series or Polymarket event title. Each row is marked supportable (the position sim can price
it once the kind exists as a kinds.toml row), needs data or model (named), or not supportable (out of
scope).
Sessions to price at. F1 already reprices after every session. NASCAR and MotoGP reprice only at fixed hours
before the race ([replay] stages). Every sport gets its session schedule in sports/<sport>.toml:
- F1: FP1–3, sprint qualifying, sprint, qualifying, race.
- NASCAR: practice, qualifying, stage 1, stage 2, race.
- MotoGP: FP1, Practice, Q1/Q2, sprint, race.
Each sport then reprices after each of its sessions, through the same stage engine.
F1¶
| Listed, not priced | Exchange | Links (races) | Status |
|---|---|---|---|
Top 5 finish (KXF1TOP5) |
Kalshi | 330 (15) | supportable now. The race_top5 kind exists; the classifier misses this series |
Sprint winner, sprint top 5 and top 10, sprint fastest lap, sprint top constructor, sprint pole (KXF1RACESPRINT, KXF1SPRINT*) |
Kalshi | 130 / 88 / 88 / 66 / 33 / 66 | supportable. The sprint stage exists since PR #73; the sprint kinds are off by default |
| Sprint winner, sprint qualifying pole | Polymarket | 199 + 69 / 159 | supportable. Same as above |
| Exact position (2nd, 3rd, 4th, 5th place) | Polymarket | about 230 | supportable. A new kind: finishing position = n |
| Constructor pole, constructor fastest lap | Polymarket | 284 / 284 | supportable. Team aggregate over the pole and fastest-lap kinds |
| Constructor score rank 1st to 5th, constructor head-to-head | Polymarket | about 220 / 36 | supportable. Team points rank and team h2h from the sim |
| Driver head-to-head and podium families with no race link ("finish ahead of", "who will finish higher") | Polymarket | 56 + 46 + 21 | linking gap. The kind exists; the link has no race_id |
| Practice 1, 2, 3 fastest lap | Polymarket | 526 / 344 / 368 | needs a model: practice pace from laps. FastF1 laps are in the database |
| Biggest mover | Kalshi | 44 (2) | needs the starting grid (core backlog C11; per-event grid penalties since PR #74) |
| Championship 2nd to 5th place (drivers, constructors) | Polymarket | 84 / 44 | supportable. Standings position from the season sim |
| Winning margin | Polymarket | 7 | not supportable now. The sim has no time gaps |
| Awards, transfers, retirements, calendar, attendance, winter testing | both | about 100 | not supportable (not race outcomes) |
| IndyCar races filed under F1 ("Honda Indy 200 at Mid-Ohio") | Polymarket | 78 | data fix. Refile them to IndyCar |
NASCAR¶
Kalshi lists, all as params.kind with prediction = 'unmodeled': win, podium, top 5, top 10, top 20, fastest lap,
pole, team win, head-to-head, biggest mover, champion and regular-season champion. Kalshi also has "win the
playoffs" (16 links) and Polymarket an In-Season Challenge winner (17 links).
| Kind | Status |
|---|---|
| Win, podium, top 5, top 10, top 20, head-to-head, team win | supportable once start lists and the global model are in (C3) and the kinds are rows |
| Pole | needs a qualifying model. 270 qualifying rounds are in the database |
| Fastest lap | needs a lap model. Laps exist for 279 races |
| Biggest mover | needs the starting grid. It's in the weekend feed |
| Champion, playoffs | needs a playoff-format season sim (16 drivers, eliminations, one-race final) |
| Regular-season champion, In-Season Challenge | supportable from the season sim once the formats are in the schema |
| Stage winners | not seen in the listings; not checked |
Other NASCAR series (Truck, possibly Xfinity) are filed under Cup; data fix first (C1).
MotoGP¶
The exchanges list race win (Kalshi 289 links, Polymarket 131), the riders' champion (Kalshi 22, Polymarket 78) and the teams' champion (Kalshi 11). Race win is supportable now. The champion and teams' champion are supportable from a season sim with a team aggregate.
Sportsbook slips (C9) list more than the exchanges do: podium, top N, head-to-head, sprint winner and pole. Those are supportable for slip EV with the model price alone. They need data: qualifying is not ingested (0 qualifying rounds in the database), so there is no grid, and sprints are only in from 2025 although they began in 2023. Both come from the MotoGP source we already use.
Engine gaps¶
These line numbers come from grep and are approximate.
- The sweep is F1-only.
weekend_sweep.pyloadssports.load("f1")at import and branchesvenue == "polymarket"else Kalshi, with no OG.com.search.pyKINDSmaps onlyf1andmtb_dh, and it branches onsport == "f1"in four places. - Paper signals are F1-only. That covers
racinglines strategy signals f1and the polymarket-or-other branches insignals.py. NASCAR and MotoGP have only the in-sample, update-mode backfill (sport_paper.py). - Sweep runs don't record the sport. No
sweeprun has asportkey inparams. - The NASCAR and MotoGP pricing model is overconfident. In the 2026-10-07 A/B/C study, the form model priced
NASCAR head-to-heads worse than uniform (log loss 1.4958 vs 0.6931). The sport-agnostic
GlobalModelwas best on the win in both NASCAR and MotoGP. See todo and the core backlog items C5 to C7 and C26. - Per-sport and per-exchange branches in
season_replay.py,season_strategy.pyandexchange_driver.pyshould become fields insports/<sport>.tomlandexchanges/<venue>.toml.
Work packages¶
Every code package is a PR into staging. A data step means a write on the VM: take a backup first, run it on the
staging database first, and add a data_changes row.
| Thread | Packages |
|---|---|
| Core (leads) | C0 coverage counter (below). C1 link cleanup: kinds from params.kind for every sport, other NASCAR series refiled, MotoGP champion links classified (data step). C2 sport-agnostic sweep: stages, kinds and venues come from the schemas, OG.com goes through venue_replay, and sweep params gain sport and venue; F1 goldens stay byte-identical. C3 NASCAR/MotoGP pricing: start lists, GlobalModel as the pricing model, and top 20, h2h, team win, fastest lap and pole as kinds. C4 CLV per bet. C5 settlement parity: one top-N rule, OG.com settle verified, mover settles. C6 cycling, gated on a Mac probe (deferred: outside this sprint). C7 live paper signals for every sport and venue. C8 the rebuild run (data step): 9 sport-venue-seasons × 9 strategies = 81 sweep jobs per model variant, then the paper backfill. C9 sportsbook slip EV for F1, NASCAR and MotoGP. This is the book map/price/settle CLI (core backlog step 8; today only racinglines/books/schema.py, the validator, exists), extended to multi-leg slips. Each leg gets the model's fair price, the linked prediction-market price where one exists, and EV against both. Kinds come from kinds.toml, so no per-sport branches. C10 linking and classifier gaps from "Every session, every supportable kind": Kalshi KXF1TOP5 and the sprint series, Polymarket families with no race link, and IndyCar links filed under F1 (a data step). C11 the supportable kinds above as kinds.toml rows and payoffs, with settlement. C12 session-aware repricing for NASCAR and MotoGP from a session schedule in each sport's schema; MotoGP qualifying and the 2023–24 sprints ingested; an F1 practice-pace model; NASCAR qualifying and lap models for pole and fastest lap |
| MCP | Sports read from the database (M6). sport and venue (including og) on edge_finder, track_record, replay_maker and run_job. list_kinds reads C0. map_book, price_book and settle_book (M9) wrap C9. The SQL guard (M1/M2) comes first |
| Web View | A coverage page drawn from C0. Sport and venue filters on Edge Finder, Strategy and Positions. Track record per venue, including OG.com |
| Web UI | Split web/app.py, then a Lab launcher for any sport × venue. og in the profile venue selector |
Order:
- C0.
- C2 and C3 in parallel, then C4, C5 and C7, with MCP and web work alongside.
- Staging data steps C1 and C8, then the owner's review.
- Prod only on the owner's decision:
vm.sh backup, mergestaging→main, then C1 and C8 on prod.
No migration is planned. No existing F1 paper rows are deleted: the rebuild writes new runs and rows.
C0: the coverage counter (spec)¶
racinglines backtest coverage is read-only. It prints, and with --out DIR writes as CSV, the matrix above from
the database and the Parquet archive, so that every tier rests on counted tape. It writes nothing to the
database.
1. Markets. One row for each sport × exchange × kind × season:
| Column | From |
|---|---|
links, races |
market_links. Kind = prediction, or params->>'kind' when it is unmodeled. races = distinct race_id |
settled_races |
races with at least one link where resolved_yes is not null |
price_races, trade_races, book_races |
races with at least one row for a linked token in market_price_history / market_trades / market_book_snapshots or in data/archive/markets/<exchange>/{prices,trades,books}/, read through markets/store.py read() |
tier |
the bar above, applied to min(settled_races, price_races). The maker counts only book_races |
modeled |
yes when the kind is in markets/kinds.py or markets/kinds.toml, and the sport's pricing_model prices it |
unmodeled_links |
links still prediction = 'unmodeled' |
Season comes from the race's event date where race_id is set, else from end_date. Season futures (race_id
null) get their own table, with links, open, settled and tape days.
2. Results depth and backfill sources (added at the owner's request on 2026-10-07): one row per sport:
| Column | From |
|---|---|
first_season, last_season, events, events_with_results |
seasons → events → races → rounds → results |
results_source, fallbacks |
sports/<sport>.toml [results] / [replay] source and fallbacks |
source_modules |
packages under racinglines/sources/ that fetch results for the sport |
cleared |
yes only when the schema has a [results] table. A sport whose schema omits it on purpose (IndyCar: the official site's terms forbid scraping without permission) shows no |
The answer to "can the Wikipedia sources backfill race data?" comes from this table. From the 2026-10-07 query:
| Sport | Results in the database | Wikipedia source |
|---|---|---|
| F1 | 2020–2026, 147 events with results | none. The official sources are already used |
| MotoGP | 2016–2026, 202 events | none |
| NASCAR | 2016–2026, 402 events | fallbacks allows Wikipedia summary-only, for missing seasons only, and never for full field rows |
| IndyCar | 0 events | racinglines/sources/indycar reads Wikipedia race pages (PR #67). sports/indycar.toml has no [results] table on purpose, so the owner has to decide whether it may be ingested |
| SailGP | 0 events | racinglines/sources/sailgp reads Wikipedia championship pages (PR #67) |
| Le Mans | 0 events | motorsport.com (PR #67), not Wikipedia |
| Road cycling | 0 events | none. Its source is procyclingstats, which is only on the owner's Mac |
More results deepen model training, which is the walk-forward burn-in. They don't add P&L backtests: those need market tape, which starts when recording began.
Done when:
- It runs on the VM and on staging. It also says whether staging's database and archive hold the tapes; that is not known today.
- Its numbers match the counts above wherever tape is not the question.
- A test pins its output on the market fixtures.
- It needs no migration and writes nothing.