Roadmap¶
The one place for what's next, across every sport and track, in priority order. Rebuilt on 2026-09-28 around the
strategy to the end of 2026 and re-ranked on 2026-10-14 around the three goals the owner set
(G1 to G3 below). Tick items off where they are listed. Finished items move to Done at the bottom. Items
tagged U1…U13 are the strategy session's app updates, C-numbers are the Core backlog's (the project
library, library/core/2026-10-06-core-backlog.md), STG-n the staging and CI items, and (F1-n) tags belong to
the F1 roadmap.
| Plan | What it's for |
|---|---|
| This page | The priorities, the race-weekend calendar, owner decisions and every open item |
| Sports and exchanges | The sport × venue grid with production's counts (links, tapes, models, backtests, paper), the gaps ranked with effort |
| Alpha layout | What the app shows today: Markets, Trade, History, Lab; styles, sizing, the leaderboard |
| Strategy 2026 | The reasoning of 28 Sep: where the demo record stood, how long each strategy takes to prove, event tiers |
| Paper trading: validation plan | The pre-registered rules every live weekend is judged by |
| F1 roadmap | The F1 phases (F1-0 … F1-9), their ground rules and decision log |
| Engine roadmap | Standard model inputs and outputs for every sport and exchange (E0 … E9), building on Backtest core |
| Parity rebuild | Every valid sport × exchange × kind brought to the F1-on-Polymarket bar; the coverage counter (C0, built) |
| Market tape | What the recorders store, every 5 minutes, every day, and how to backfill a gap |
Priorities¶
Where things stand (14 Oct 2026). racinglines.bet runs the fantasy league alpha:
every signed-in account gets F$1,000 of fantasy money and four tabs, Markets, Trade, History and Lab
(Alpha layout). Hand trading is F1 only, on the recorded Polymarket and Kalshi books; Markets shows
F1, NASCAR and MotoGP. Polymarket lists F1 race markets again (rounds 13 to 20 are linked); no real order is placed
(the trading flags are unset, Sports and exchanges). The three recorders poll Polymarket, Kalshi and
OG.com every 5 minutes, every day. From the command line, --remote, racinglines remote and racinglines data
pull SPORT reach the hosted server, in testing, with a warning that the review of venue and data terms is still
pending (MCP server).
The goals (owner): G1, October to early November, prove the edge, closing-line value above +2% over 100 live
bets; G2, November to December, real fills, venue capacity, more users; G3, January to March 2027, billing.
The real-money gate stays at six or more live weekends. What is in flight now, in order: the staging-to-production
promotion (PR #180: MotoGP for every signed-in user, data pull, remote, race-data triggers); CLV on every fill;
the calibration check of every sport's model (C48); the Probalytics tape comparison (PR #181); the one engine for the
first package release (C56).
P0 · now (14 Oct to the United States GP, 25 Oct)¶
Each item names its PR or backlog number; "promotion pending" means it is on staging and reaches production with
the next staging → main promotion (a draft until the owner says "promote"; the owner merges).
Owner, 2026-10-23 (after the alpine build, docs/alpine.md): three items in this order, each its own thread.
- [ ] U-F1-DC · P0 · Driver vs car: are the F1 models over-weighting car strength and under-weighting driver skill?
Study done 2026-10-24 (reports/2026-10-24-f1-driver-vs-car/REPORT.md;
teammate_h2hkind in the walk-forward, 847f9126): right in direction, modest in size: production model k = 1.23 [0.96, 1.56] (1.86 in pairings 0-4 races old), the 0.60-0.70 band under-separated (+0.086 ), Max/Tsunoda 2025 and Russell/Antonelli under-separated, Max/Hadjar 2026 not. Structural blind spot confirmed on Bottas 2026 (priced 0.475 all season vs Perez, realized 0.235): the laps core's driver term is relative to the previous teammate, so skill does not travel. Remaining: the fixes as a sweep (driver_prior_n, a driver-level offset prior; rank_logit pair_sd), owner decision on any default. Original brief: Hypothesis (owner): the Max-vs-Isack pricing gap is too small while Lando-vs-Oscar looks right. Structural reason to expect it: the sector/laps core fits car pace per team (ridge_team) and driver skill only as a driver-vs-teammate offset shrunk toward zero withdriver_prior_n = 3races, so a new pairing starts the season priced as equals and skill never travels between teams; in rank_logit the grid / qual_gap covariates carry combined car+driver pace and leave theta little to explain. Cars do explain most of the finishing order, so the symptom lives in within-team markets, not podium log loss. Study (backtests, saved runs first):* (1) a teammate head-to-head diagnostic from the saved walk-forward predictions of 2025 and 2026: model P(A beats teammate B) vs realized, bucketed by predicted probability and by pairing age (new vs established; Red Bull's second seat gives three lopsided pairings against one reference driver); (2) ateammate_h2h_loglossmetric beside win and podium inwalk_forward_events.csv(the pairwise operand wanted since alpine) so sweeps can score it; (3) if confirmed: walkdriver_prior_n(3 toward 1) and a driver offset prior seeded from the driver's prior-team record instead of 0 (laps core), a driver-level prior on theta andpair_sdcarrying the car residual (rank_logit), each scored on teammate head-to-head log loss on held-out seasons, never on podium. Falsifier: lopsided pairs calibrated after 5+ races together = cold-start only (a prior-from-history fix); miscalibrated all season = the car/driver split itself. Owner decision on any default change (decision log). Roles: decide Fable (the split and the rule), own Opus (diagnostic + metric), implement Sonnet (settings + tests). Files:models/inputs/laps.py,models/rank_logit/model.py,pipelines/sweep_settings.py, the walk-forward writer,tests/; golden tests byte-identical until a promotion. - [ ] U-F1-SW · P1 · F1 sweeps on the updated sweeper, with the driver-vs-car findings. The alpine walker v3
(
scripts/research/alpine_walk.py: compass + pattern + hammer moves, dynamic steps, rungs, multi-seed sigma with a noise floor, fresh-seed confirmation of every move, a check set, merit over a fixed race count, resumable state, evals.csv provenance) pointed at F1 (--sport f1): the F1 settings as VARS, the teammate metric as a merit option, the pre-registered held-out rule and the gate / report-card / luck-test reports reused as they are. Runs on the production VM under the one-core / low-priority guards (owner, 22 Oct) or on rl-alpine restarted. Output: the F1 report card in-sample and held-out, decision-log entries for any promoted setting. -
[ ] U-SWEEP · P1 · One sweeper routine for every sport, now and new, and the alpine data made general. Promote the walker from
scripts/research/into the package (racinglines sweep walk <sport>: VARS from the schema's[search]ranges, arms from[sport] challengers+ the default core, merit and check set as settings, every surface: CLI, MCPrun_job, Lab form, a test per surface); fold the alpine night's helpers (gate, coverage, report card, golden run, holdout analysis, +EV board) intoracinglines report-style commands; make the alpine pieces general:[model] history = "category", the run-times core's schema binding,soelden_book.pyas a sport-agnostic "price an upcoming event with owner-supplied book lines" command (exclusions, name matching, book letters). Then the alpine follow-ups in docs/alpine.md: season futures, start lists, EC/WJC data, the ensemble with its own held-out, and the PR oftrack/modelsinto staging. -
[x] Promote staging to production (#180), 2026-10-14. Merged 06:50Z, prod deploy green 06:55Z (806278c). Checked 17:00Z: live auto on, no per-event F1 timer, nothing paused;
record fastf1disabled (no race this week). The internal book is indata/internal-docs/on both sites (vm.sh internal-docs, 410 files). - [ ] Next promotion: staging → main (draft until the owner says "promote"). Carries #188/#189:
record_fastf1.shexecutable (vm.sh record fastf1 statusand the recorder's unit failed with "command not found" without it) and two data-change entries. Touchesscripts/vm/, so it deploys production. - [ ] Stage cutoffs from a session's actual end, not its scheduled end (owner, 2026-10-14: P0, review after the
day's fill-fidelity runs). A stage's cutoff is the scheduled start +
[sessions.schedule]minutes +[stages] lag_minutes30 (racinglines/core/stages.pybuild), and the model's data gate uses the same scheduled end (models/inputs/laps.pysession_end). A session that overran by more than 30 minutes (red flags, a delayed start) is then priced with results the market had not seen yet: a future leak in every backtest and sweep, worst at "after Quali". Same on both tapes, so the ours vs Probalytics A/B holds, but absolute P&L can be too high. First step: list every 2026 session whose actual end (FastF1 session status, VM) is after its cutoff; then cut from the actual end where it is known. The fill paths themselves read as of t (tested intests/test_book_fill.py). - [ ] Backfill session ends, then gate every model's history by settlement (owner, 2026-10-22).
Separate reviewed change, always on for every model and raw/calibrated arm together, not a temperature-only
switch. Backfill NASCAR ends from
run_date_utcandtotal_race_time, and MotoGP session ends; audit missing timing before choosing a fallback. Include base fits, side/retirement histories, covariates and calibration history, with same-weekend and late-settlement leakage tests. Back up before database writes and log the data change. The current seven-run ranking batch deliberately retains the existing event-date rule. - [ ] CLV on every live and replayed fill (C4).
racinglines backtest clvcovers stored paper bets; the sweep stats,f1 reconcile,f1 scorecardand the weekend report still read P&L only. G1 is judged on CLV: the plain mean per decided bet against the fill price (owner, 2026-10-07). Paper trading. - [ ] Calibration by probability bucket, every sport (C48; owner, 2026-10-08: the next development once staging
is confirmed). Favourites, mid-field and long shots, walk-forward against results and against closing prices
where recorded, for F1, NASCAR, MotoGP, downhill and road cycling. Evidence: Indonesia MotoGP, the model's top
three summed to 42.5% against Kalshi mids of 83.5%. Until it is done NASCAR and MotoGP prices are labelled
unvalidated and no pick rests on them alone. Models.
F1 (2026-10-14): the sweep setting
calib_power(walk_forward: fitted on earlier races only), off by default, better log loss and P&L in 2025 and 2026 sweeps (F1 calibration). Open: make it the default (owner), the 10–20% bin still under-priced, the other sports. - [ ] Probalytics tape: pull, compare, golden (#181, into staging). Resumable F1 pull by exact key (owner,
2026-10-14: F1 complete, unlinked F1 markets included as data only, then NASCAR; any other sport only on the
owner's explicit go);
RACINGLINES_TAPE_SOURCE=probalyticsas a backtest venue; pass marks mid within 1¢ on 95% of snapshots, weekend P&L within 10% or $5, trades within 5%. The golden comparison runs onracinglines_staging_demo. The internal recorder stays primary (owner, 2026-10-12). - [x] Staging as a live mirror of production (#154), on since 2026-10-14 17:54Z (scope
all; 42 tables ready, 0 bytes behind at 18:31Z; data changes). Open: the production backup's exact file name fromvm.sh backup staging-mirroris not yet in the data-change entry. - [ ] Staging mirror upkeep (#154; on since 2026-10-14 17:54Z, 42 tables, 0 bytes behind). No more
staging resetwhile it is on. After any production migration, runvm.sh mirror status; a table production adds joins only aftervm.sh mirror onruns again (it refreshes the publication). Owner OK still needed to delete the pre-mirror leftovers on the VM:racinglines_staging-before-mirror-*.sql.gzunder staging'sdata/backups/db/anddata/archive.pre-mirrorif it exists. - [ ] Which database staging serves. Still the season demo (
racinglines_staging_demo), which the Probalytics comparison (15 Oct) uses; the mirror fillsracinglines_staging.vm.sh staging db mainto show the mirrored copy,staging db demoto go back (owner's call). - [ ] Road cycling on production. The Kalshi road cycling links (53 events, 9,421 links, 14 Oct) are only in
racinglines_staging_demo. Next:vm.sh backup road-cycling-kalshi, thenracinglines markets --exchange kalshi --sport road_cycling sync --closedon production; the mirror then carries them toracinglines_staging. Fair prices still need the PCS results ingested and a forecast (Road cycling); admin only. - [ ]
vm.sh internal-docshas no staging-only mode. It always copies to production and staging; a staging-only copy was done by hand on 14 Oct. Low priority. - [ ] United States GP weekend (23–25 Oct), the first live weekend of the alpha with Polymarket race markets
(266 links stored) and Kalshi's (expected 2 to 4 days out). A, C and K paper-trade; the fantasy accounts trade
by hand or follow a style; no code merges in the book window; Monday:
f1 reconcile,f1 scorecard, and the first DITL report for an F1 weekend. - [ ] Sign-up terms acceptance (
TERMS_VERSION, planned in Fantasy accounts, not in code). The data-terms review is done (owner, 2026-10-14: venue prices to signed-in users, MotoGP in the alpha anddata pullall allowed; the #aintcare warning stays); the sign-up page still records no acceptance.
P1 · to the sizing review (~12 Nov)¶
- [ ] The one engine for the first package release (C56). Every modelled sport already prices with
GlobalModel; what remains is folding the laps core's qualifying, grid and practice knowledge into the results core (F1 race-win Brier 0.1134 vs 0.1476 in the 2 Oct validation). ThePositionSimandTimedRunsclasses and their import aliases were retired on 2026-10-19; the cores stay. Owner chose "compare first" (2026-10-11; the laps-core Lab jobs re-run 2026-10-19 as jobs 14 and 15); open: the "works the same" tolerance and the timing against G1 and the Las Vegas GP. Scope: the library'score/2026-10-11-retire-f1-model-scope.md. - [ ] Recorder follow-ups: read
backfill_tape.shresults into the data-change log after each gap; alert when a recorder misses a pass (the 1 to 2 Oct Kalshi outages were found a week later by the Probalytics comparison). - [ ] NASCAR before the Chase finale (8 Nov): the pre-race field from NASCAR's entry list (C46, Mac probe first), start lists in the replay (C5), the replay's race spread (owner decision), and a replay on Polymarket's own NASCAR tape, recorded since 7 Oct.
- [ ] MotoGP after the calendar lands: sprint classifications (Mac probe first), champion classifiers and a season sim (Championship markets).
- [ ] STG items still open (Staging and CI deploys): STG-5 off-VM copy of the VM's
data (the disk is still the only copy), STG-7 fixtures for CI, STG-8 migration gate, STG-6, STG-9, STG-10,
STG-11. STG-2 (tests in CI) is partly done: CI runs pytest on PRs (PR #180 reports it green) but
deploy-productionstill does not need it (C17). - [ ] Per-user bet books in the app (C55) and the F1 race-week demos (C54), after the alpha's first weekends.
- [ ] Sizing review (~12 Nov) after 4–6 live weekends on real markets (Paper trading).
- [ ] Web: race URLs by sport (owner, 2026-10-14): race pages under their sport (
/f1/races/135style) instead of/races/{id}, with the old URLs redirecting (thelegacy.pyMOVED pattern) and template links updated. Not started. The race page 500 behind the ask (an all-null Parquet column) is fixed in #184. - [ ] Web: docs icon in the top bar (owner question, 2026-10-14, unanswered): the book icon beside GitHub links to
/docs/today (#184). Open: switch it to the Jupyter Book logo and link the owner's Jupyter Book once it is hosted (it is only served locally so far).
P2 · to the Las Vegas GP (21 Nov) and year end¶
- [ ] Las Vegas GP (21 Nov): the milestone the owner set for real stakes ($2k to $10k, CLV-gated); only if the validation rules and G1 are met, and never without the owner's explicit sign-off on the trading flags in the same turn.
- [ ] Qatar (29 Nov) and Abu Dhabi (6 Dec) may be cancelled: settle by each venue's rules (U8; the owner's two assumptions still to confirm).
- [ ] Road cycling: the stage-race GC model (draft #177, held by the owner), its calibrated settings' decision-log
rows (road noise 1.25, ITT noise 1.15,
road_gap_scale0.1,short_tt_noise_scale2.0), a results source whose terms allow it, then a first VM data step (backup first). Admin-only on the board until then. - [ ] PyPI release (preview written 2026-10-11; no PR until the owner's go): ships the single engine (C56), Rich output by default, the kart demo offline, then our MCP by sign-in, then self-hosted.
- [ ] 2026 season report (December); walk-forward re-selection of the profiles for 2027; U10 pre-season testing ingest; U12 Google Cloud; U11 downhill off-season (Points validation, Data, Model); decide whether NASCAR and road cycling are 2027 sports.
Record: the sprint to the soft launch (28 Sep – 8 Oct 2026)¶
Kept as the record of that sprint; everything ticked below is in Done with its PR.
- [x] Finish CI and the staging environment: STG-1 (staging host), STG-3 (staging deploys), STG-4 (owner checks staging, then promotes) built; the rest is in P1 above.
- [x] Sign-up built (#146, the alpha fantasy launch on production 2026-10-12, with the fantasy reset).
- [x] Recorders on the VM: Kalshi and OG.com books every 5 minutes (U2, 30 Sep; every day since 2026-10-07), trades every 15 minutes, Polymarket NASCAR and MotoGP books (U9, #91), the forecast-refresh timer, the schedule-sync timer (#169).
- [x] Round 16 live paper (Sepang, 2–4 Oct) and round 17 (Singapore, 9–11 Oct) ran on Polymarket's and Kalshi's real markets; the Singapore /term parts are recorded (#157).
- [x] Coverage gaps in the 3 × 3: Polymarket MotoGP links (209 on production); MotoGP calendar ingest built (#134, promotion pending); the OG.com column on the VM.
- [ ] Champion replays after deploy (#95, #96): merged 30 Sep, no run recorded (Data changes).
- [x] Progress lines on every long job (#99, #114).
- [ ] Report reproducibility (REP-1 to REP-7):
SOURCES.mdby hand on every report since; the rest open (Reports). - [x] Fantasy soft launch: shipped as the alpha layout (#160, #161) on 2026-10-12, F$ only, F1 hand trading.
Where things stood on 30 Sep, 29 Sep and 28 Sep is in Strategy 2026 and the 48-hour report.
Race weekends to 31 December¶
One row per event, in date order, with what production already holds for it (2026-10-14). Every F1 weekend also runs the championship sleeve after the race, and the weekend routine.
- [ ] 18 Oct · NASCAR Phoenix (Freeway Insurance 500). Kalshi 200 links, Polymarket 43. Results core price (unvalidated); the timers record and fetch results.
- [ ] 23–25 Oct · F1 R18, United States GP. T1 on Polymarket (266 links) and Kalshi when it lists. A, C and K paper; the alpha's hand traders and styles. 25 Oct · NASCAR Talladega.
- [ ] 30 Oct – 1 Nov · F1 R19, Mexico City. T1 (Polymarket 266 links). NASCAR Martinsville.
- [ ] 6–8 Nov · F1 R20, São Paulo. T1 (Polymarket 266 links). NASCAR finale, Homestead (8 Nov). MotoGP's November rounds are recorded as T2 once the calendar is on production.
- [ ] ~12 Nov · Sizing review (Paper trading).
- [ ] 20–22 Nov · F1 R21, Las Vegas (Saturday race). T1; check the night-time stage cut-offs; the CLV-gated milestone.
- [ ] 27–29 Nov · F1 R22, Qatar. T1 once U8 is confirmed; may be cancelled.
- [ ] 4–6 Dec · F1 R23, Abu Dhabi. T1 (1 Kalshi link stored already); championships settle.
If Polymarket stops listing again: Kalshi carries the record. If neither venue lists an F1 race: that weekend is T3 (the private book, admin-only) and proves the pricing, not an edge. There are no downhill events left in 2026.
Weekend routine¶
For every F1 weekend, T1 or T3:
- [ ] Wed: check Kalshi and Polymarket for the race's markets; confirm they link to the event (not
unmodeled);vm.sh record statusandvm.sh pm-sync statusshow no WARN. - [ ] Thu 18:00 PDT (or the equivalent before FP1): pick the tier and log it in the decision log.
- [ ] Race weekend: the signal engine (T1) or the live engine (T3) runs unattended; no code merges in the book window except logged fixes (staging deploys are allowed).
- [ ] After the race: the signals pass settles the paper positions and stops (C59, promotion pending); run the championship sleeve.
- [ ] Monday: the reconciliation (live vs replay, T1) and the pricing scorecard (T1 and T3); the DITL report; flag a weekend outside the validation rules and explain it before the next.
Owner decisions¶
Open decisions first, then the ones on record (so a thread never re-asks one).
| Decision | When | Recommendation / status |
|---|---|---|
| Merge the promotion (#180) ⚠️ VM scripts and units | Now | Yes; then the post-deploy checks in P0 |
| C56: the "works the same" tolerance for the one engine, and whether the F1 switch waits for the Las Vegas GP | Before the first package release | Validate now, switch after Vegas (proposal, 2026-10-11) |
| Probalytics download beyond F1 and NASCAR | Per sport | Only on the owner's explicit go for that sport (2026-10-14) |
| Sizing rule | ~12 Nov | After 4–6 live weekends on real markets |
| Real stakes at the Las Vegas GP (21 Nov) | After G1 | Only if CLV > +2% over 100 live bets and the validation rules hold; the flags need the owner's word in the same turn |
| U8 rules for a cancelled race: Polymarket 50/50 on head-to-heads, Kalshi NO versus void | Before Qatar (29 Nov) | Confirm against each venue's rules text |
| Replay race spread for NASCAR (2.0 places vs a measured ~7) | Before any NASCAR result is read as edge | Owner's call, with a decision-log entry |
| Road cycling: hold or merge the GC model (#177); which results source | Any time | Held by the owner (2026-10-13); decision-log rows first |
| Add NASCAR (and road cycling) for 2027 | December | Only if the tapes, calibration and data terms hold up |
Promote gridq+pretrain as the F1 default; keep reset for 2027 |
December | Decide on the walk-forward; live prices already use gridq+pretrain+reset through the profiles |
--unraced-format last as the downhill default |
Any time | See Model |
| STG-5 off-VM data copy (VM service account write access to the bucket), STG-7 fixtures in git or bucket, STG-8 migration gate | High | Yes; STG-5 first: today the VM's disk is the only copy |
| OG.com: confirm the $0.02 fee; per-GP contract rules | Before any OG.com result counts | From OG.com's fee schedule and the two recorded race weekends |
| History purge for data off GitHub (force-push) | Last stage | Needs the owner's spoken OK in the same turn |
| Open-Meteo's free API is non-commercial only (Weather forecasts) | Before billing (G3, 2027) | Stay on the free tier while there is no revenue |
Keep or retire the /term browser replay, now that the quickstart and demo pages show captured commands and outputs (scripts/docs/capture_examples.py) |
Any time | Keep for now; retire once the docs pages cover its three parts (docs review, 2026-10-14) |
The docs' mailbox addresses (Email setup) and the cloud service-account name (Data) are also in the public /llms-full.txt |
Any time | Already public in the repo; owner to confirm they may stay, or the llms hook redacts them |
| Odds presentation names in the sportsbook schema (Sportsbook schema) | Any time | Owner checks the names; a rename is one line in racinglines/books/schema.py plus the docs table |
On record (don't re-ask): no sport-specific models, only schemas that declare model inputs (2026-10-11, C56);
no freezing of strategy profiles, every change logged with its date (2026-10-05); live books never freeze
(2026-10-04); the real-money gate stays 6+ live weekends (2026-10-05); market prices polled every 5 minutes every day
(2026-10-07); the internal recorder stays primary, Probalytics is a reference tape (2026-10-12); venue prices, MotoGP
and data pull may reach signed-in users, the #aintcare warning stays (2026-10-14); MotoGP visible to the alpha
(2026-10-14, reversing the 11 Oct hide); loss cap and tier for round 16 (decided in the
F1 live test decision log); Kalshi bid/ask candles re-pull automated on the VM
(2026-10-05); new VM timers one at a time, each with a backup (2026-09-30); the OG.com column on the VM (read-only,
2026-10-01); Fable only when the owner asks for a project-notes update (2026-09-30); close draft #97, superseded by
110 (2026-09-30).¶
Exchanges¶
Phase F1-9, pulled forward: Kalshi is the likeliest venue with live F1 race markets this year.
- [x] U2 · P0 · Kalshi recorder on the VM. Done:
racinglines-record-venues.timer(scripts/vm/record_venues.sh) snapshots every open Kalshi and OG.com market every 5 minutes, every day since 2026-10-07 (books since 2026-09-30: 923,282 Kalshi snapshots in production's Postgres on 2026-10-14), syncs hourly, stores the last two hours of trades every 15 minutes, and reads OG.com's settlements hourly (Market tape). Original plan:markets --exchange kalshisync every 30 min on race weekends, plus trades, books and history. - [x] U1 · P0 · Kalshi in the signal engine (PR #34, merged 2026-09-29) (F1-9's "done when").
pipelines/signals.py,taker_weekendandmaker_replayreadexchange='kalshi'markets for a profile whose newvenuesetting says so (defaultpolymarket, optional, left out ofsettings_keywhile unset). Signals carrydetail.venue='kalshi'and positionsvenue='kalshi'. Group by market ticker (token_id), never bycondition_id; applyKALSHI_MAKER_FEE; the liquidity filter is per market. Polymarket profiles unchanged. The sweep reads Kalshi the same way. Done when: a replay of 2026 round 15 through the engine on Kalshi matchesf1 demo-history --venue kalshifor C to the cent, and A's Kalshi taker replay matches the sweep's trades (scripts/signals_parity.py --venue kalshi). No order is sent;KALSHI_TRADING_ENABLEDstays unset. - [x] U5 · P1 · Kalshi sprint markets (PR #36, merged; on by default since 2026-10-06,
RACINGLINES_KALSHI_SPRINTS=0turns it off). Map sprint winner and sprint pole (KXF1*sprint tickers,unmodeledtoday) to the sprint stages (after SQ, after Sprint). Check on the Dutch GP (round 12). Done when: Singapore's Kalshi sprint markets sync with a model price. - [x] U7 · P1 · Cross-venue disagreement log (PR #35, merged, behind
RACINGLINES_DISAGREE; Polymarket and Kalshi only, OG.com not joined yet). For each F1 outcome linked on both venues (race and championship), keep both mids, both fee-adjusted edges and the top of each book at every recorder pass (a table or a view overmarket_price_history), with a report (racinglines markets disagree --event …) and a panel on the Markets page. Done when: the report lists the gaps above fees for the championship markets, day by day. - [ ] OG.com follow-ups (new, from Coverage): (a) done: the 5-minute recorder covers OG.com's books, trades and settlements since 2026-10-08 (Market tape); (b) confirm the $0.02 fee in OG.com's fee schedule (S); (c) per-Grand-Prix
[[rules]]inexchanges/og.tomlonce a live race market is seen (S); (d) join OG.com to the U7 log (S–M). (e) done: a/markets/ogpage and a generic per-schema-exchange route, plus/markets/tapes(PR #49; found by the 2026-09-29 spot-check, Coverage). (f) outcomes:markets --exchange og settlereads the settlement feed (get-expired-settlement-price, probed from the Mac 2026-10-06), hourly inrecord_venues.shsince 2026-10-08 (Exchanges); on 2026-10-14 production holds 108 OG.com F1 links, 44 of them per-GP race markets (Malaysia and Singapore, closed), so (g) per-GP contract rules can now be written from the recorded names. (h) a replay on the two recorded race weekends: not run. - [ ] Coinbase F1 weekend patch (temporary): a public GraphQL fallback for this weekend's F1 race markets and event page, shown behind a schema switch and a short-term TODO. Future work: formal market mapping, driver/team resolution, stable persisted-query tracking, and a proper auth/key path if Coinbase exposes one for deeper history.
- [ ] Other exchanges, if they list motorsport or cycling markets with real depth, including one for downhill (none on Polymarket or Kalshi as of 2026-09).
- [x] OG.com (Crypto.com's CFTC prediction market): connector built as a schema (PR #44:
exchanges/og.toml, one generic driver,racinglines markets --exchange og sync|trades|history|books|fair; off by default). Open: a recorder timer on the VM (the API keeps about a month), per-GP contract rules once a live race market is seen, confirm the $0.02 fee against OG.com's own fee schedule (today from reviews; the API terms were checked), join the U7 comparison. Trading needs FCM onboarding and the owner's API key (Coverage).
Paper trading¶
Phase F1-8, now on every venue that lists; how it works: Paper trading. Recommendations and paper fills only; no order is placed.
- [ ] P0 · Pre-register the rules; profiles stay tunable (owner, 2026-10-05: no freezes). A, C and K can be re-tuned at any time. Each change is recorded with its date in the F1 roadmap and starts a new settings version with its own live count. Rules: validation plan.
- [ ] Run A (demo taker) and C (demo maker) live on every weekend Polymarket or Kalshi lists, through 2026 and into 2027. Built and scheduled on both venues (the signal engine, every 5 minutes in a race window); ran on rounds 16 and 17 (last stored signal: Singapore after qualifying, 2026-10-11 14:37Z). Under the alpha, every account's followed style is a paper profile of its own, sized to F$1,000 (Alpha layout).
- [x] U3 · P1 · Kalshi maker profile K (sweep done; freezing is the owner's call). A sweep of the maker's settings on Kalshi's 2025–26 tape
(spread,
max_disagree,size, modelgbm/gridq+pretrain, per-marketmin_volume_24h), judged on both seasons asparams-4hwas. Save the winner as the Lab candidate "K · Kalshi maker", assigned as the demo maker's Kalshi profile. Done when: K is frozen before round 18, with its 2025 and 2026 Kalshi replay P&L recorded in Kalshi history. Result (PRs #38, #41, corrected 2026-09-29): K =gbm, 2¢ quotes, 10-point disagreement filter, 25 shares, $400 24-hour volume floor: +$490 (2026), +$659 (2025), at 16,000 simulations +$453 / +$741, against profile C's −$119 / +$510. The first 2025 figure (+$855) missed Imola 2025, which only Kalshi listed (−$195). Owner (2026-10-05): K's settings stay tunable; log each change in the decision log and keep its live record per settings version. - [x] P1 · Per-weekend reconciliation (PR #37, merged).
racinglines strategy reconcile f1 --event 2026-NN --profile X --venue V: live paper fills and markouts against the replay of the same weekend on the recorded tape (the conservative "through" fill rule), flagging a weekend outside the validation rules. Its output goes into the weekend's report. - [x] P1 · Pricing scorecard (PR #28, merged:
f1 scorecardfor exchange weekends; run it on every weekend), traded or not: Brier and log loss of the fair values against the result and against each venue's mid at each stage.racinglines live reportdoes this for private books; extend it to Kalshi and Polymarket weekends. - [x] P1 ·
track_record(venue='all')in the MCP server (PR #30, merged; original problem: lists one row per weekend while its total sums every venue (the maker's weekends mix Polymarket and Kalshi rows). Return one row per weekend and venue, with avenuecolumn and a total per venue. - [ ] P1 · Closing-line value (CLV) in backtests and live (owner, 2026-10-05). For every replayed and live
fill, record the venue's price when the market closes (lights out for race kinds, the session start for
pole), and report CLV = close minus entry, signed for our side, in points and in percent. Report it per
profile, venue, market kind and weekend, next to P&L in the sweep stats,
f1 reconcileandf1 scorecard. Polymarket tapes first. Kalshi uses the recorder's bid/ask books from 2026-09-30 on; Kalshi history waits for the bid/ask re-pull, because its last-trade candles spike. Hand-placed sportsbook bets get the same field in the project's bet ledger, using an exchange's close as a proxy where the book publishes none. Done when: the 2025 and 2026 replays of A, C and K report mean CLV, and the weekend report shows it. Paper bets done (C4):racinglines backtest clvreports CLV for every stored paper bet, any sport and venue (CLI). Still open: the sweep stats,f1 reconcile,f1 scorecardand the weekend report. G1 definition (owner, 2026-10-07): the go-live gate reads CLV as the plain mean per bet against the fill price, every decided bet counting once (the wider gate: more bets, even if the P&L reads less sharp). Storing sweep trades one row per trade, so sweeps get CLV too, is a nice-to-have (core backlog C41). - [ ] P1 · On-demand strategy previews (owner, 2026-10-06). One command that prices the coming weekend
from a stored stage run and shows what each strategy (T1–T10, TB) would enter now, at a chosen bankroll
and caps (half- and quarter-Kelly). It writes two tables per strategy: the markets it would pick (side,
shares, cost) and every other modelled market with its skip reason, such as no edge, the volume
filter or a cap. It must be read-only, with no orders and no database writes, and it marks
apparent-value trades that fail a filter as watchlist only. First version (PR #64):
scripts/strategy_preview.py, with tests anddocs/strategy-preview.md. It's standalone and read-only, works for any event and source run at 4k or 16k, and was first run on Singapore 2026-10-05 at $1,000, $50 a market and $250 in total. Every trade there was blocked by zero recorded 24-hour Polymarket volume, so the preview should also show the venue's own 24-hour volume next to ours. Done when: it's aracinglines f1subcommand documented indocs/cli.md, and it shows the venue's volume. - [x] P2 · Stale line in
docs/coverage.md(owner, 2026-10-05). Fixed 2026-10-14: production holds Polymarket race links for every 2026 round from Italy (13) to São Paulo (20); the page's counts are now dated. - [ ] P2 · Sizing review (~12 Nov), after 4–6 live weekends on real markets (T3 weekends don't count):
walk-forward re-run of A, A-lite, C, K, B (#06) and A′ (#08) with rounds 16–20 added, then a sizing
rule from
bankroll/max_deployed(owner's call, e.g. A-lite → A). - [ ] Use it live: the signal engine sizes from the account's balance and deployed capital, once the review picks a rule. Later, fractional sizing once recorded depth supports it.
Live events¶
How it works: Live events. First run: the Whistler downhill final, 27 Sep 2026. Live events are T3 unless a venue lists the event.
- [x] F1 live test, round 16 (Bahrain GP at Sepang, 2–4 Oct 2026) and round 17 (Singapore, 9–11 Oct): both venues listed, so both ran as T1 paper weekends on the signal engine (Polymarket 183 and 272 links, Kalshi 165 and 314, OG.com 22 each); the Singapore picks and their path through the sessions are the /term parts 2 and 3 (#157). The private-book runbook stays ready for a weekend no venue lists: F1 live test.
- [x] Live timers are calendar-driven:
vm.sh live <event>turns onlive autoinstead of a per-event timer (#134, promotion pending), and the signals pass stops once the race result is in and the positions are settled (C59, #175, promotion pending). - [x] P1 · Launch specs for rounds 18–23 (PR #24, merged) (
live/f1/2026-18.toml…2026-23.toml), used only on weekends no venue lists. Check Las Vegas's Saturday-night stage cut-offs. - [x] U6 · P0 · Championship sleeve, paper only (PR #29, merged; first live rebalance is after round 16).
f1 season-strategyrun live after each race on both venues (Kalshi'sKXF1*champion markets included), as a separate profile. Its positions stay out of A, C and K's track records. The 2026 replay was −$208 (the model lagged pre-season), so expect little. Done when: round 16's post-race rebalance is stored for both venues. - [x] U8 · P1 · Relocated and cancelled races (PR #27, merged, behind
RACINGLINES_CANCELLED_RACE_RULES; the Polymarket 50/50-on-cancel and Kalshi NO-vs-void rules are assumptions for the owner to confirm). Encode Kalshi's and Polymarket's rules for a cancelled or moved race (Polymarket's Bahrain 2026 "Other" resolution; the Abu Dhabi "take place" market) in settlement, with a test on Bahrain 2026. Done when: paper positions on a cancelled race settle as the venue's rules say. - [ ] Owner: switch the loss cap on for round 16 or not (
max_loss = 500inlive/f1/2026-16.toml[live.quoting]). - [ ] Optional, round 16: record SignalR during FP1 with FastF1's client, to learn whether it needs a login.
- [ ] A live timing feed (F1 SignalR or OpenF1) for in-session updates; compared in the F1 live test plan.
- [x] Downhill
live run: take a lock in the run folder like F1's step, so a restart can't leave two loops polling (what happened at Whistler, 22:38–22:39). Before the 2027 downhill season.
Market making¶
- [ ] The stage-aware taker on the live weekends, before anyone uses it (it lost $316 out of sample on 2025).
- [ ] First real V2 order (owner): post, list and cancel one small post-only order, unverified
against the live CLOB so far.
POLYMARKET_TRADING_ENABLEDstays unset until then. Only after the validation rules are met. - [ ] Replay with recorded book depth (F1-4): queue position and competing makers,
instead of the touch/through bounds. The queue model is built (
--fill queue, queue rule, tested on synthetic books, 2026-09-28, cloud build-out); running it needs several weekends of recorded books (Polymarket and, with U2, Kalshi). - [ ] Liquidity rewards as a replay P&L line; optional fractional-Kelly caps.
- [ ] Record venue liquidity rewards (owner asked 2026-10-13; on hold, not merged): branch
claude/project-thread-bg60pcaddsracinglines markets --exchange polymarket|kalshi rewards(markets/rewards.py), run hourly by pm_sync.sh and record_venues.sh: each venue's reward programs on our open markets, a row only when one starts, changes or ends, todata/archive/markets/<exchange>/rewards/; an empty venue response is never saved. No migration. Next: rebase on main, open the PR. Probe 2026-10-13: Polymarket set min size 50 / max spread 4.5¢ on 219 of 241 markets per race (Austin, Mexico, Brazil), but only one Austin market paid ($0.001/day); Kalshi had no F1 race markets or incentive programs yet (the last ones: Bahrain pole, target size 1,000, $100 per market). Re-check Kalshi/incentive_programs?status=upcomingfrom about 21 Oct.
Models: the one engine¶
Owner rule (2026-10-11): no sport-specific models, only schemas that declare model inputs and compatibility. Every
modelled sport prices with GlobalModel and its schema's [model] inputs picks the core (F1 laps and sectors, downhill
run times, the rest results); the old classes are challengers (Backtest core,
Sports and exchanges: model status).
- [ ] C56 · retire the sport-specific classes for the first package release. The results core must take
qualifying, grid and practice as schema inputs before F1 can switch without losing accuracy (race-win Brier
0.1134 laps core vs 0.1476 results core, 2 Oct validation); downhill's
TimedRunsthe same way (C56c). Owner: compare first; the F1 walk-forward comparison is production Lab jobs 1 to 4 (2026-10-11; two done, two failed). Live F1 and downhill pricing unchanged until the owner's go. - [ ] C48 · calibration by probability bucket, every sport (the next development, owner 2026-10-08): favourites, mid-field and long shots, walk-forward against results and against recorded closing prices; a schema-set temperature or shrink toward the market if it fails, with a decision-log row. NASCAR and MotoGP prices are unvalidated until then.
- [ ] NASCAR and MotoGP in the results core: start lists instead of every driver ever seen (C5), fitted noise and
retirements (C6;
GlobalModelhandles retirements, C26), a real challenger (C7), head-to-head and top-20 kinds live (C10), the pre-race field from the entry list (C46),race_start_utcfor CLV (C42). - [ ] A naive baseline (engine E7):
market_implied; Plackett-Luce was tested on 7 Oct and lost to the probit engine in every sport, so it is not the baseline (Novig and the bake-off); the extended model, onerank_logitchallenger with ties, teams,top_kand a covariance, is F1-6, started narrowed on 2026-10-14 (decision log). - [ ] F1-6 · the
walk_forwardjob has nosettingsparameter. MCPrun_joband the Lab form take a challenger by name (model) but not its settings, so rank_logit'stop_k,tie_on,team_effectsand the rest reach only the CLI (backtest walk-forward --model rank_logit --top-k 3) and the search jobs. The Lab also lists every challenger for every sport and rejects a mismatch only at submit. Needed before the F1-6 comparison can be run from the Lab orracinglines remote(every-surface rule). - [ ] Prediction records, cross-engine pairing (found by the F1-6 comparison, 2026-10-14): record
subjectkeys differ between engines (the results core andrank_logitwrite"1", the run-times core"ath:1"), soeval compareacross engines pairs nothing; andmarkets.kinds.settlereturns False, not None, for an entrant the result doesn't know, so such rows score as losses without warning. One key convention for every writer of records, andsettle→ None, with a test each. - [ ] F1-6 confirmation on NASCAR and MotoGP (pre-registered in F1-6):
after
racinglines data pull nascar|motogp --no-marketsinto the pull database (owner sign-in), threerank_logitruns per sport againstglobal,race_poleincluded; then a decision-log row fortop_k/qualifying if they hold. - [ ] Decision-log rows still missing: in-race
SIGMA = 3.0and the live downhill constants (C12); the road cycling GC settings (#177).
F1 model¶
Phased plan and ground rules: F1 roadmap.
-
[ ] Local Jev auto-sweeper (owner, 2026-10-15;
track/models-jev). Finite supplied pool, exact sweep-key dedup, target/held-out search-report evidence, bounded runs/API calls and interruptible durable resume. Code-only planner and mocked runner tests are built locally; a real Jev/sweep end-to-end check still needs an owner-selected pool and local database backup. No automatic promotion, VM action or real trading. Usage and scope. -
[ ] P1 · Grid penalties (branch
work/grid-penalties). Confirmed penalties insports/f1/grid_penalties.tomlmove the race's or sprint's starting grid before it is simulated (F1 model, "Grid penalties"); first row: Russell to the back for Singapore (2026-17). Re-price Singapore once it ships. - [ ] P2 · Weather-aware position-model variant
gridq+pretrain+reset-WX(owner, 2026-10-06: "later branch", not the weather PR). The disruption mixture (more noise and retirements in disrupted races) weighted by the race's forecast rain probability (weather/wet.p_wet_series, lead 5 days) instead of history alone; then the strategies on it (A-WXagainstA) in their own sweep under the promotion rule. Until then a-WXstrategy name would be byte-for-byte its baseline: nothing weather-aware feeds win, podium, h2h, constructor or pole. Decided after the 5-day forecast backtest found the forecast beats the circuit history for rain, is neutral for red flags and adds nothing for safety car or DNF (Weather forecasts, decision log). - [ ] U13 · P2 · Sprint-race pricing model. In progress (2026-10-06, branch
work/sprint-stage). Until U5 (PR #36) Kalshi's sprint winner and sprint pole markets priced from the race's win and pole probabilities as a stand-in; the owner approved that for Singapore (2026-09-29). Landed:- The sprint stage (F1 model, step 6). It is
schema-driven (
[sessions.sim]insports/f1.toml) and drawn on a side stream, so Grand Prix prices are byte-identical. It prices from the GP finishing model with the sprint points anddnf_scale0.5 (provisional, decision log). - Grid: SQ's laps ranked by best valid lap once SQ has run, else simulated. After the sprint, its
actual result. Per year (
grid_from_by_year): the 2021 sprint takes its grid fromqual(no SQ then). - Outputs:
extra.sprint_win_prob/sprint_pole_probon every run of a sprint weekend; sprint arrays in the sims and records. - Kinds:
race_sprint_podium/_top8/_h2h/_constructor_top(default=False; out of every book per DEC-12), settled fromrace_outcomes' sprint columns. - Polymarket:
<GP>: Sprint Winnerand<GP>: Sprint Qualifying Pole Winnerlink asrace_sprint_win/race_sprint_pole. - Walk-forward:
racinglines model sprint-check f1, run on the owner's Mac (2026-10-06): the stage beats the GP-win stand-in once the SQ order is known (decision log). Open: - Sprint backfill on the VM:
f1 fetch --sprints-from 2023for the 12 missing 2023–25 weekends (a database write: backup first, data-changes entry). Then refitdnf_scaleand decide whether a sprint-specific grid weight or noise earns a setting. - SQ order: segment-aware SQ1/SQ2/SQ3 ordering (best lap per segment, not over the whole session)
and a TeamId fill for SQ rows (FastF1 leaves both empty). Also SQ positions in
ingest.py, if FastF1 ever gives them. - Settlement timing:
race_sprint_polesettles from the sprint's starting grid (extra.grid, after penalties), so not before the sprint has run. - Live stage runs before SQ only see the sprint when the caller passes
stages=(the weekend's rows don't exist yet): wire it from the schedule insignals/weekend_sweepif sprint kinds go live. - Replay against the Kalshi and Polymarket sprint tapes. Done when: sprint markets price from the sprint simulation and beat the race-odds stand-in on the walk-forward and the tape replay.
- The sprint stage (F1 model, step 6). It is
schema-driven (
- [ ] U10 · P3 · F1 pre-season testing: ingest FastF1's testing sessions as a stage before round 1 for
the forecast and the season strategy, so the pre-season forecast sees what the market sees.
Done when: a 2026 re-run with testing moves the pre-season constructors' odds toward the market's
(Mercedes 41%), measured with
f1 compare. - [ ] Promote
gridq+pretrainto the default (owner's OK: it changes live prices; re-run the sweep andUPDATE_GOLDENin the same change). Owner, 2026-09-27: keep iterating first; decide in December on the walk-forward. The paper-trading profiles pick their model through settings. - [ ]
resetfor 2027, the second year of these regulations: keep it in profile A's model or not (December). - [ ] Decide whether profile A's model takes
rookie(gridq+pretrain+reset+rookie, run 1289), after live weekends. Not before 31 Dec: A is frozen. - [ ] Teammate-battle uncertainty: a larger per-driver season drift, or a driver-form model.
- [ ] Props next: calibrate fastest lap on stored stage runs; map Polymarket's and Kalshi's prop markets
in the sync classifiers (they're
unmodeledtoday) and backtest against their prices. - [ ] Fastest lap from the simulation (
fastlapvariant, off, 2026-10-04): price stage runs with it, scoreextra.fl_probagainst Kalshi's resolvedrace_fastest_laplinks on the VM, then the owner decides whether a profile takes it (decision log). Scoreflpos(2026-10-07: the fastest lap from the simulated finishing order and pace) the same way, beside it.
New sports¶
F1 (large audience, deep markets) and UCI downhill (niche, no exchange markets) are the two endpoints. Candidates between them: NASCAR, MotoGP, WEC, IndyCar, Formula E, road-cycling classics and grand tours, XC/enduro, alpine skiing. Rank by audience, data availability and exchange listings; no grassroots or local-league plans. As of 2026-09 the ranking is led by exchange depth: Strategy 2026.
- [x] U9 · P1 · Tape recording for other sports (code, PR #26, merged). First live run on the VM, 2026-09-29: Kalshi NASCAR 13,599 links and 1.69M trades, MotoGP 322 links and 12k trades, IndyCar 1,374 links and 167k trades, all
unmodeled(Data changes). NASCAR and MotoGP tapes were pulled again for the replays in the overnight VM run (2026-09-30). The tapes timer isrecord_venues.sh(every 5 minutes, every day, since 2026-10-07). Generalise the Kalshi sync to named series: NASCAR Cup (KXNASCAR*, race and champion), MotoGP (KXMOTOGP*), IndyCar (KXINDYCAR*). Each under its own competition (asports/<code>.tomlentry with no model), linksunmodeled, with prices, trades and books archived per exchange. Done when: the NASCAR finale (Homestead, 8 Nov) and MotoGP's Qatar–Valencia rounds are recorded from listing to settlement. - [x] Polymarket tapes for NASCAR, MotoGP and IndyCar (PR #51). The Polymarket sync now takes a named tape-only
sport (
[markets.polymarket] tagsinsports/<code>.toml, linksunmodeled, additive: no reset or delete path). Running on the VM inpm_sync.sh(PM_SPORTS): production holds 1,721 NASCAR and 209 MotoGP Polymarket links on 2026-10-14 (Coverage). - [x] UCI road cycling, tape only (schema, PR #50):
sports/road_cycling.toml(competitionuci_road_wt, KalshiKXCYCLING*), plusle_mans.tomlandsailgp.toml(Kalshi, and SailGP on OG.com). Not yet run on the VM:markets --exchange kalshi --sport road_cycling sync --closedpulls the 2026 grand tours Kalshi still serves. A model is a 2027 question (Sports and exchanges).
MotoGP prediction model track¶
The database and API plumbing are in place and the source gate is complete: the public MotoGP results API is verified for season/event/standings/classification data, and the first pass stays within that contract. Status 2026-09-30: sports/motogp.toml now has model_family = "position_sim" and pricing_model = MotoGPRaceChallenger (a result-only form model, Sunday race only), the taker replay runs it (racinglines strategy replay motogp, #82), and the board's race-win column is on by default (owner, 2026-09-30). That happened before M5's gate was met: the model has not been compared with a naive baseline, the Kalshi replay has no held-out season (2025 lists no MotoGP race markets) and its ranking has not been read. Read its prices as a baseline, not an edge (Sports and exchanges). Since then: pricing_model = GlobalModel (decision log 2026-10-07), and with the one engine (staging, 2026-10-11) model_family = "global" at the results level ([model] inputs = ["results", "qualifying"]); MotoGPRaceChallenger is kept as a challenger (One engine).
- [x] M0 · MotoGP source gate: verify the public API and document the actual data that exists (
api.motogp.pulselive.com, race classification and standings only; no JSON lap or sector splits). Record the limits in Data (the MotoGP limits paragraph itself is still to be written there). - [x] M1 · MotoGP model skeleton (
models/motogp_model.py; circuit history not used): create the sport-specific model settings + variant plumbing in the same schema as the F1 sweep (sweep_settings.py/f1-style run params), but without the F1-only assumptions. Base the first variant on rider form, team-level strength, and circuit history using the already-ingested results table. - [ ] M2 · Feature builder for race results only: keep the first model conservative: only result-based priors (prior race form, rider/team recent pace, circuit-adjusted history), no lap-by-lap or sector splits because the public API does not expose them as JSON. Lock the feature set to what is available in the standard schema and document the exact leak-free cutoff rules.
- [x] M3 · One-season backtest harness (as the shared taker replay,
pipelines/position_replay.py, one pre-race pricing per race rather than F1's stages; saved runs aremodel_runskinddiagnostic): wire a MotoGP backtest using the F1 walk-forward pattern (before qualifying / before race) but with the MotoGP event calendar and the same result-kind conventions. Save a small set of model runs todata/runs/motogp/and verify the model can price a handful of old races without crashing. - [ ] M4 · Sweep then compare: run a narrow sweep over recent seasons (for example 2021–2026, or a 2–3 season window) and compare the baseline variant to a simple form-only challenger. Use the same search/backtest/reporting conventions as the F1 path, even if the first pass is intentionally small and not production-quality.
-
[ ] M5 · Promotion gate: only if the backtest beats a naive baseline on a consistent set of races and there are no leakage issues should MotoGP move from
model_family = "none"to a live pricing model. The first pass stays conservative and does not change any trading or live market plumbing. -
[ ] Tape-sync follow-ups (2026-09-29 first live run): ~~the sync should seed a sport's competition row from
sports/<code>.tomlinstead of raisingNoResultFound~~ done: every sync creates the rows of the sport it files links under (db/ingest.ensure_competition), andvm.sh deploynow runsdb seedafter the migrations; exchange schemas can state the exchange's own caps (limits), checked when the schema loads. Kalshi'sKXNASCARTRUCKSERIES(35 links) andKXNASCARAUTOPARTSSERIES(40) match theKXNASCARprefix and landed undernascar_cup; the owner chose to tag them (2026-09-29), not filter: done by the market-link pass (params.nascar_series, applied to the stored links byracinglines data link nascar). Still open: the Polymarket tag slugs above. - [x] NASCAR and MotoGP in the app (#108–#112, deployed 2026-09-30): the Markets "Every sport: status" table
(
RACINGLINES_SPORT_STATUS=1), NASCAR and MotoGP sections with replay-save prices, next races and recent results, sport filters on the strategy pages, and the demo accounts' NASCAR and MotoGP paper rows labelled "demo replay, in-sample" (RACINGLINES_SPORT_PAPER=1,nascar|motogp demo-history, run on the VM byvm.sh demo). Kalshi: NASCAR Pro −$278.31 (2025) / +$6,525.45 (2026), Basic −$357.23 / +$2,657.23; MotoGP 2026 Pro +$31.25, Basic +$7.70. In-sample: the settings were picked on the same seasons. - [x] Next-race forecasts (#112):
racinglines model forecast nascarstored run #9068 (South Point 400); the board and race pages show its fair price. Undo:racinglines model forecast nascar --undo 9068. - [x] Forecast refresh after each race:
racinglines-forecast-refresh.timerevery 30 minutes fetches and ingests NASCAR results on race weekends and Mondays, refreshes the NASCAR and MotoGP forecasts, and re-runs the F1 championship forecast once per new race result (#107; the daily backup it takes is logged in Data changes). Still open: the NASCAR pre-race field from the entry list (C46). - [ ] MotoGP calendar ingest: built in #134 (the forecast-refresh step fetches MotoGP's calendar and results daily, every pass on MotoGP weekends; the probe passed), promotion pending: production holds no MotoGP round after Austria (18 Sep) on 2026-10-14, so MotoGP has no next-race price until #180 deploys and the first pass runs.
- [x] Polymarket MotoGP links: 209 on production (131 with a race, 29 open, 2026-10-14), synced by
pm_sync.sh; no replay or demo backfill on them yet. - [x] Champion replays (#95 F1 on Kalshi and OG.com, #96 NASCAR and MotoGP
season-replaybehindRACINGLINES_SEASON_REPLAY=1; merged 2026-09-30). Left: deploy, then the VM steps inhandoffs/2026-10-01-season-replays.md. MotoGP flags SPRINT POINTS MISSING until sprints are ingested. - [ ] Read the unread overnight outputs: MotoGP's grid ranking (re-run after #102), the OG.com buy-all CSVs (a plumbing check, never a result), the NASCAR spike share.
- [ ] Re-run the coverage probes and spot-check every combo (handoff written 2026-09-29:
handoffs/2026-09-29-spot-check-exchange-coverage.md) (Kalshi and Polymarket counts drift; the remaining ? cells) on the VM or the owner's device: the read-only commands in Coverage. Effort S. - [x] P2 · NASCAR data sources (research, PR #25, merged): results, qualifying, practice and lap data (NASCAR's public feeds, community archives), their terms and history depth; written up in Data.
- [x] NASCAR results adapter (
racinglines data fetch nascar | ingest; the feeds probed 2026-09-29, findings in Data). Done on the VM 2026-09-30 (backup first): 408 Cup events 2016–2026 ingested and 7,544 Kalshi links identified with a race (Data changes). The original plan: database backup, then Cup 2017–2026 on the VM with adata_changesentry (three races were fetched on the Mac on 2026-09-29 and the pit feed read: real stops,nascar_pit_data_2026_5628.json). Built since: scheduled events for upcoming races and the driver/race resolver (sources/nascar/identity.py); the market-link pass (sources/nascar/links.py,racinglines data link nascar, and inside the Kalshi, Polymarket and OG.com syncs): race by name within its season, driver, contract kind, and acup | xfinity | truckstag, withpredictionleftunmodeled(tested on the real Kalshi and Polymarket listings). Next, in order (VM, owner): full pull andingestafter a backup; thenlinkdry run, read the unresolved list,link --apply --backup FILE; then the readiness audit. OG.com's NASCAR contracts and Kalshi head-to-heads were sampled from the Mac (PR #68) and the pass handles both. - [ ] P3 · NASCAR for 2027? Decide from the recorded Chase tapes (volume, spread, how often markets trade) and the data review. If yes: a first model in Jan–Feb, paper-traded from the 2027 Daytona 500.
- [ ] MotoGP: its qualifying / sprint / race weekend maps onto the F1 adapter; sprints are not ingested yet, so the model is Sunday-race only and the champion is quote only. Thin on Kalshi (champion ~$20k); revisit if the tapes show depth.
- [ ] Skip horse racing: the Interstate Horseracing Act keeps it off Kalshi and Polymarket.
Points validation¶
The points tables in racinglines/models/inputs/run_times/ (FINAL_POINTS, QUAL_POINTS,
QUAL_POINTS_ROUND) are placeholders, and the same tables are used for every
season. Until they're fixed, every number measured in points (expected points,
standings, champion odds, spearman_points) is approximate. Off-season work (U11).
- [ ] Get the official UCI DHI World Cup points scales, from the UCI MTB regulations (Part 4) or the standings PDFs on ChronoRace. Get the 2026 table first, then earlier eras for backtests (2021–22 single qualifier, 2023–24 semi-final, 2025–26 Q1/Q2).
- [ ] Replace
FINAL_POINTSwith the official Final points for every scoring place. Check how many places score: currently 30, but the 2021–22 Finals had 60+ riders. - [ ] Confirm which qualifying round pays points in each format, and how many places
score. Also check whether riders who get through Q2 score. Update
QUAL_POINTSandQUAL_POINTS_ROUND. If more than one round pays, extendactual_event_pointsandsimulate_weekend. - [ ] Check the edge cases:
- [ ] DNF/DSQ in the Final: zero points, or last-place points?
- [ ] Ties
- [ ] Protected or wildcard riders
- [ ] Bonus or double-points rounds
- [ ] Reconcile:
racinglines data points mtb_dh check --season 2026 --through-round 7 --standings …compares each rider's cumulative total with the official standings (built; needs the official tables and standings). Every rider should match exactly. Repeat for one past season. - [ ] Re-run
seasonandbacktest, and refresh the tables in the docs and README.
Data¶
Downhill data, mostly from ChronoRace (prod.chronorace.be), which the cloud network blocks: allow it in
the environment first, or run locally.
- [ ] Elite Women and Junior Women: download (
mtb_dh download --category 'Elite Women'already works; ChronoRace is blocked in the cloud, so locally), then forecast with--unraced-format last. Done (2026-09-28, cloud, synthetic data): unraced rounds can take the category's own format (--unraced-format last, off by default); the default simulates every category in the 2026 elite men's format (a 30-rider final), which put all 24 riders of a synthetic women's field in the final. Enter the official formats per category insports/mtb_dh.toml[rounds.category_format](owner). - [ ] Start order: it's in ChronoRace's start-list PDFs and possibly the JSON. Needed for track-evolution and weather effects. Blocked in the cloud (ChronoRace).
- [ ] Weather and track conditions (the PDFs have weather;
--conditions-fileexists but is empty). Blocked in the cloud (ChronoRace). - [ ] Check the incomplete past seasons: UCI 2023 shows 7 of 8 events completed and 2025 shows 9 of 10 (a cancelled round or a missing result each).
- [ ] 2019–2020 PDFs: the text layer is unreadable font codes, so they'd need OCR. Low priority.
- [ ] Re-create event diagnostics and scenarios where needed after the 2026-09-27 database rebuild (optional; sweeps, runs and market links came back from the snapshot).
Model¶
Downhill model.
- [ ] Check the new downhill defaults on 2027's first rounds (out of sample; 2026 is over).
- [ ] Owner: make
--unraced-format lastthe default? Juniors' unraced rounds are simulated in the elite men's 30-rider-final format today; with their own format, the 2026 MJ leader's make-Final odds for the next round go from 92% to 73% (committed snapshot, 2,000 sims). Changes junior and women's prices. Per-round-type incident rates tried too: no gain. - [ ] Protected-rider rules in older formats.
- [ ] 2027 venues: once the calendar is announced, use venue history in the forecast.
- [ ] Decide whether to retire or rebuild the older
fit/predictElo/GBR model. It hasn't been checked against the multi-season data.
Engineering¶
- [x] Engine roadmap E0: owner decisions D1–D6 taken as recommended 2026-09-29 (decision log); capability matrix written.
- [ ] Engine roadmap, window 1 (read-side, now to 6 Dec), one branch and PR per task, in order: ~~E1a sport frames for F1 and downhill~~ (done, PR #73:
racinglines/frames/,[data]in the schemas) · ~~E4a long prediction records besiderace_predictions~~ (done, PR #76, behindRACINGLINES_PREDICTION_RECORDS) · E1b market frames over the canonical price reader · E1c NASCAR frames (after the onboarding thread'snascar ingest) · E2DataView.asofand the generic leak guard (switch off; ⚠️pricing.py) · E5racinglines eval· E6racinglines report buildand the model card (the plan: Reports REP-3 to REP-6) · E7market_impliedandplackett_luceon the NASCAR tape and F1, with a NASCAR model card for the 2027 decision. Details: the owner's Engine Implementation Plan page. - [ ] Engine roadmap, window 2 (after 6 Dec, before 2027's first race): E3 contract v2 (⚠️ live sweep and signals path) · E4b readers onto the records plus the
predictionstable (⚠️ VM migration, backup, sign-off) · E8 consumers read the registry · E9 Kalshi and Polymarket classifiers to per-sport TOML rules, fees from the schema, D5 (⚠️ money-adjacent). Theexchanges/kalshi.tomlandexchanges/polymarket.tomldrafts can be proved in a scratch database this year. - [ ] E2 prerequisite for downhill: an
events.end_datecolumn set at ingest. The frames parse a weekend's end from the event name (racinglines/frames/mtb_dh.py) becauseeventshas onlystart_date, which is not always the weekend's first day. A DB migration: backup first (the standing practice in CLAUDE.md) and the owner's sign-off. - [ ] Data defect, F1 event 27: its qualifying rows alias BEA to placeholder athlete 567 and miss Stroll, so the entry list (and the E1a
entrantsframe, which uses it) lacks him. Found in E1a; not fixed there. - [ ] U12 · P3 · Move to Google Cloud, after Abu Dhabi so nothing changes under live weekends: the data bucket is up (Data) and the proposal is written (Cloud SQL, Cloud Run, Scheduler, service identities, about $28–35 a month at list prices). Next: the owner's choices (database tier, recorder shape, domain).
- [x] Forecast runs from committed code: both live forecasts (F1 run 3, downhill run 1287) were built from
1ab8934-dirty. Checked (2026-09-29): the F1 forecast does not overwrite run 3;f1 forecast --savealways inserts a newmodel_runsrow (save_model_run, nothing updates or deletes forecast runs; Lab promotion only setsparams.promoted_at) and the web app prices from the latest one. Run 3 is the only F1 forecast because the database was rebuilt on 2026-09-27 (ids 1–3 are its first rows, 18:37–18:42Z) and the forecast has been saved once since; the downhill one came later (1287). Still to do: re-save both from committed code. - [x] Web app N+1 in
model_prob(PR #43, merged 2026-09-29): Kalshi'smarket_linksrows doubled the per-market lookups on the Markets page; fixed with one batched read. - [x] Same-second maker fills kept apart in stored signals (PR #39, merged), the bug
f1 reconcilefound. - [x] CLAUDE.md (PR #45, merged): the standing process note (environments, model tiers, dev cycle, pre-merge checks, report format, safety rails). Open: its Fable tiering versus the session-only no-Fable rule.
- [ ] Remove the
demo_contextbubbles before real users. - [ ] Before anything goes beyond a private demo: check F1's data terms, OpenF1's non-commercial terms, and settlement rules for relocated or cancelled races (U8).
- [ ] JSON API, owner: what's public (results only, or prices), who can use it (accounts, keys, no login), data terms, rate limits (open questions).
- [ ] Condition in-weekend forecasts on completed rounds (downhill). Once Q1 has run, fix who has qualified and use the Q1 times. Also use split times from disrupted Timed Training sessions.
- [ ] Market-making loop (F1-5, deferred): re-quote linked markets automatically when the model or book moves, within per-market and total exposure limits.
- [ ] Have
forecast_seasonreadscheduledevents (no start list yet) as named rounds instead ofremaining_round. - [ ] Ingest on a schedule: download, then
racinglines data ingest mtb_dhfor new rounds. - [ ] Ingest the other input formats (copy/paste, HTML, JSON) into the database.
- [ ] Tests: parser fixtures (one file per format era),
event_format,actual_event_points, the rider-ID normalization check. - [x] Faster searches (2026-09-29). Every search job was one CPU-bound Python process with the database
idle, and a full-season Kalshi maker job spent ~60% in the maker replays, ~20% reading the weekend's
markets, ~15% loading Kalshi's tape, plus the rating history. Now: (1)
[search] grid = Nruns up to N sweeps of one season and model in one process (f1 sweep --grid), sharing the measurements, stage pricings, markets and maker tape, with saved runs identical to separate processes; (2) the rating history is built only when a stage has to be priced, and[search] history_cache = truekeeps it on disk (data/cache/history/, keyed by model settings and data); (3)paralleldefaults to every core. The 106-jobsweeps/kalshi-maker-k.tomlgrid went from 59 to 21 minutes on 4 cores, same results. Left: the maker replay itself (maker_replay.quote/PublicView, about half of what remains).
Web UI: open items (from the "Web UI Improvements" thread, closed 2026-10-14)¶
Interaction side of the web app (forms, auth and sessions, navigation, app-shell code). The full backlog with audit
citations lives in the project library (library/audit/2026-10-06-web-ui-backlog.md). Checked against staging
at 4905943 on 2026-10-14 where marked; the rest were last checked on 2026-10-06.
- [ ] S1 · CSRF is one global token (
CSRF_TOKEN,check_csrfinracinglines/web/app.py), the same for every user and visible to the demo logins. It guards settings, MCP and admin writes and the order routes. Still true 2026-10-14. Money-adjacent: its ownwork/web-securitybranch, staging first. - [ ] S2 · Login throttle trusts
cf-connecting-ip/x-forwarded-forfrom any client (users.py,app.py), per-IP only, in-process dicts that never evict, no test. Still true 2026-10-14. - [ ] S3 ·
APP_SECRETunset falls back to a random per-process secret (app.py: sessions and CSRF break on restart). Fail fast instead. Still true 2026-10-14. - [ ] S4 · What sign-up's role may trigger (DB-writing syncs,
diag_example). Needs the owner's call; re-check against the alpha layout, which runs basic as pro. - [ ] S5 · Admin "create user" does not validate the role (
admin.pyadmin_user_create; the update path does). Still true 2026-10-14. - [ ] S6 · Demo guard redirects to the
Refererheader (demo.py:80, off-site redirect possible). Still true 2026-10-14. - [ ] S7 · Logout and password reset don't end other sessions (stateless cookie). May need a per-user counter column: a migration, so backup and owner sign-off.
- [x] U1 · Self-serve strategy switching for pro users. Done 2026-10-14:
/strategy/pickfor every account (alpha layout), and the same pick from MCP (fantasy_strategy,fantasy_follow) and the CLI (racinglines remote call fantasy_follow), one shared path infantasy/follow.py. - [ ] U2 · Password change and a light/dark toggle in Settings (
settings.htmlhas neither, 2026-10-14). - [ ] U3 · Link the pro/basic comparison from the sign-in page.
- [ ] U4 · Open the sport with a race in the next 7 days on /markets without overriding remembered state (shared with the Web View side).
- [ ] C2 · Web app sport-specific literals (Lab job types,
'f1_wdc'in views and templates) to schema-driven, per the one-engine rule. C1 (splitapp.py) is mostly done: 883 lines on 2026-10-14, down from 1,550. - [ ]
/demo/bookand/demo/livepages for the F1 race-week demos (Core C54; planlibrary/engineering/2026-10-11-f1-race-week-demos/plan.md, the Web UI side owns the replay behaviour). No such routes onstaging2026-10-14. - [ ] Maker
/positionspage speed: the profilerlibrary/web/2026-10-11-positions-speed/profile_positions.py(run on the VM from the Mac) has never been run or its output never returned. - Standing limit (owner, 2026-10-11): further maker UI is drop-down entries and sub-setting inputs only, no new components. Alpha settings stay as they are until about mid-November (owner, 2026-10-12).
Staging and CI deploys¶
High priority (owner, 2026-09-30; in P1). Audited 2026-10-01 against the public repo, the
workflow, scripts/deploy/, deploy/vm/ and the data bucket (VM deploy: Code from GitHub, data on the
VM).
What works today (2026-10-01). main-merge-gate (.github/workflows/predeploy.yml): detect-docs-only skips
everything when the change set is only docs/ and *.md; otherwise predeploy-staging smoke-checks
staging.racinglines.bet, and on a push to main deploy-production runs vm.sh deploy <sha> (pause timers,
untrack, checkout, pip, alembic upgrade head, db seed, restart, resume timers) and smoke-checks
racinglines.bet. First green end to end: run 19, commit 4469100.
What it does not do yet, and what to watch until it does:
| Gap | Effect now | Watch for | Fixed by |
|---|---|---|---|
| No tests in CI | A green PR has only passed a smoke check of staging | Run racinglines check, pytest -m "not live", mkdocs build --strict locally before asking for a merge |
STG-2 |
Golden tests skip without tests/fixtures/ |
~50 tests skip on a public checkout, all goldens included; pytest still looks green | Pricing / model / replay PRs: run the goldens on the owner's Mac | STG-7 |
| Staging smoke doesn't run the PR's code | predeploy-staging checks whatever staging serves |
Merge the branch to staging first (PR 11) and look at it; predeploy-staging alone means "staging is up" |
STG-4 |
| A merged migration runs on the VM at once, no backup step | The safety rail (owner sign-off for VM migrations) depends on who merges | PRs with migrations/versions/*: ⚠️ in the title, vm.sh backup <purpose> first, owner's word, then merge |
STG-8 |
| No off-VM copy of the VM's data | VM disk loss = data since 2026-09-28 gone, incl. archived market history | Avoid anything risky on the VM's disk; vm.sh backup before every data step |
STG-5 |
| Cloud sessions read the old bucket (last push 2026-09-28) | Cloud sweeps and analyses run on stale data | Say so in any cloud result; check bucket.py ls dates |
STG-5 |
| No concurrency group | Two merges close together run two vm.sh deploys at once (overlapping pause/resume) |
Merge one PR, wait for its Actions run to finish, then the next | STG-6 |
| Every code merge deploys, also in a live window | The web app restarts; live timers pause and catch up | Don't merge code during a book (round 16: Fri 2 Oct 03:30Z to Sun 4 Oct 07:00Z) | STG-9 |
/docs is built on the VM by deploys (since PR 7) but not by docs-only merges; /pitch images are only on the VM's disk |
/docs lags docs-only merges; a rebuilt VM has no pitch images |
After a docs-only merge: vm.sh docs |
STG-10 |
| Re-tracking a file the VM keeps untracked | Checkout refuses after untrack.sh; timers stay paused until fixed |
Before committing pitch images or fixtures, move the VM copies aside | STG-10, STG-7 |
| Claude GitHub App not on the public repo | PR and CI events never reach threads | Threads check PRs by hand; owner installs the app | STG-11 |
| Workflow edits | Cloud sessions can't push .github/workflows/* |
Workflow changes come as a patch the owner applies (git am) |
— |
Each item below needs the owner's go; the ones touching the VM, DNS, the bucket or secrets need sign-off.
- [x] STG-1 · staging host. Built 2026-10-01 (PR 11, VM deploy: Staging): a second app copy on the VM (port 8010,
racinglines_staging, no timers, no trading flags),vm.sh staging setup|deploy|status|reset|off. Open: Cloudflare Access in front of it. Original plan: a second app instance with its own database (racinglines_staging, restored from the latest VM backup, trading flags off,RACINGLINES_*switches as on production), either a second compose project on the VM (cheapest; the n2-standard-2 has room) or a small separate VM; Cloudflare DNS and tunnel route; Cloudflare Access or basic-auth. Owner decisions pending: same VM or its own, keyless GCP auth (workload identity) or the JSON key, Cloudflare Access or a password. - [ ] STG-2 · tests in CI (workflow patch). A
checksjob on every PR, required beforedeploy-production: Python 3.14 as on the VM,pip install -r requirements.txt -e ., a Postgres service container withTEST_DATABASE_URLpointing atracinglines_test(neverracinglines: the fixture drops the DB it names),racinglines check,pytest -m "not live",pip install -r requirements-docs.txt && mkdocs build --strict. It must not run on docs-only change sets' deploy path, but it should still runmkdocs buildfor them. Done when a PR that breaks a test is red on GitHub. - [x] STG-3 · deploy branches to staging. Built 2026-10-01 (PR 11): a merge or push to the
stagingbranch runsdeploy/ci/staging.yml(vm.sh staging deploy <sha>, smoke withSMOKE_EXPECT_ENV=staging); the owner copies it into.github/workflows/. Original plan: on a push to atrack/*branch (or a PR label), CI deploys it to staging with avm.sh deploy --stagingvariant (alembic runs against the staging DB only), runssmoke.shagainst staging and posts the result on the PR. Replaces today's staging smoke of whatever is live. - [ ] STG-4 · verify, then production. The owner checks staging (a short checklist per track: pages load, Markets board counts, paper rows), then merges; the merge deploys.
- [ ] STG-5 · an off-VM copy of the VM's data. (2026-10-14: built as
scripts/vm/offsite.sh+racinglines-offsite.timer,vm.sh offsite; nightly prod dump,data/backupsanddata/archive/marketsto the primary bucket. The VM can already write there. Restore drill and cloud-session read key still open.) Today the VM's disk is the only copy of the newest database,data/backups/, and the market history the recorder's archive pass moved out of Postgres intodata/archive/markets/(dumps alone no longer hold it). Steps: (1)bucket.sh pushgets a VM mode:pg_dumpthroughdocker compose exec -T dbinstead of the Mac'sPG_BIN; (2) the VM service account gets object write onracinglines-data-384052502248(owner, GCP; GCE's default scope is storage read-only, so the VM may need a scope change and a restart); (3) aracinglines-offsite.timernightly (outside book windows): dump, trailer check, upload the dump + manifest andgcloud storage rsyncdata/archive/markets,data/runs/live,data/raw, the pitch images; progress line every 5 minutes; (4)vm.sh offsite [status]to enable it and read the last run; (5) a restore drill:vm.sh restoreinto a scratch database on the VM, row counts compared. Then decide whether cloud sessions get an HMAC key on the VM's bucket (read-only) or the VM also pushes to the old bucket. Never at the same time as a VM DB write step. - [ ] STG-6 · CI hardening (workflow patch):
concurrency: { group: deploy-production, cancel-in-progress: false }ondeploy-production; the docs-only rule narrowed todocs/,README.md,CLAUDE.md,mkdocs.ymlandhandoffs/(today any*.mdanywhere counts); a job summary naming the deployed sha and the timers resumed. - [ ] STG-7 · test fixtures for CI and cloud. The public repo has
tests/golden/but notests/fixtures/. Owner decision: commit them (they come from public sources: FastF1, Polymarket, ChronoRace; check size and terms) or keep them in the bucket and pull them in CI with a read-only key. Before committing, move the VM's untracked copy aside (else the next deploy stops afteruntrack.sh). Done when a fresh clone runs the goldens instead of skipping them. - [ ] STG-8 · migration gate.
update.shdumps the database (data/backups/db/racinglines-before-migrate-<UTC>.sql.gz, trailer checked) wheneveralembic currentdiffers fromalembic heads, beforealembic upgrade head; and the workflow refuses to deploy a commit that addsmigrations/versions/*unless the PR carries an owner label (e.g.migration-ok). Keeps the "no VM migration without the owner's sign-off" rail true under auto-deploy. - [ ] STG-9 · deploy freeze for live windows. A
/var/lib/racinglines/deploy-holdfile on the VM (set byvm.sh hold on|off, or automatically while aracinglines-live-*timer is active) that makes the CI deploy wait or fail cleanly with "held for a live event"; a manualworkflow_dispatchre-runs it after. - [ ] STG-10 · built docs and pitch images on the VM. Done for code deploys (2026-10-01):
vm.sh deployandvm.sh docsrundeploy/vm/build_docs.sh(/docswas 404 "Docs not built" on the VM since cutover). Open: docs-only merges run a light workflow job that callsvm.sh docs(workflow patch). The 10 pitch images go to the bucket (STG-5) and, if the owner wants them in the repo, are committed underracinglines/web/static/pitch/with a.gitignoreexception (VM copies moved aside first). - [ ] STG-11 · GitHub App. Install the Claude GitHub App on
racinglines-public(owner: https://github.com/apps/claude/installations/select_target) so PR, review and CI events reach the threads.
Reports: consistency and reproducibility¶
Owner ask (2026-09-30): reports lean on ad-hoc Claude work; someone else (or a later session) can't rebuild one. Recommendations, in order; the first two are cheap and can start with the next report.
- [ ] REP-1 · a
SOURCES.mdper report folder (now, by hand; in CLAUDE.md): the commit, every command or SQL query behind a number or plot, run ids, data files and their hashes, screenshot settings. - [ ] REP-2 · one report skeleton. A template with the fixed sections both good reports share (scene-setter, top-line table, sections, timeline, winners/losers, "what this does and doesn't show", what's next) and the screenshot rule (1024 px, 150%). Reports keep their own voice inside the sections.
- [ ] REP-3 ·
racinglines report build <folder>(engine roadmap E6, with the model card). A folder holdsreport.toml(inputs: run ids, queries, date window, venue, sport; seeds; the settings file), the Markdown with placeholders for tables and plots, andbuild.pyhooks. The command re-runs the queries, draws the plots with fixed styles, fills the tables, renders HTML and PDF throughmarkdown_html.render(aslive reportdoes), and writes amanifest.json: commit, inputs, output hashes. - [ ] REP-4 · pinned inputs. Reports read a data snapshot, not the live DB:
db exportof the tables they use (or the bucket's Parquet snapshot) named by date and hash inreport.toml. Model runs by id, never "latest". Seeds for every simulation in the report. - [ ] REP-5 · versioned config and model cards. Every tuned setting read from a committed settings file with its decision-log entry; a model card per model in use (inputs, training window, calibration, known gaps), linked from each report that uses it.
- [ ] REP-6 · regenerate check. A CI job (after STG-2) rebuilds one reference report from its pinned
snapshot and fails if a number changes;
UPDATE_GOLDEN=1-style promotion when a change is meant. - [ ] REP-7 · screenshots by script. A Playwright script per report takes its screenshots at 1024 px and 150% against staging (after STG-1), so they can be re-taken, not re-clicked.
Internal onboarding book (2026-10-14)¶
The admin-only onboarding book (Jupyter Book) is served at /docs/internal/ from data/internal-docs on the VM
(route in racinglines/web/info_routes.py, copied by vm.sh internal-docs, on prod since #180). Its source is
never in git: it lives in the project library (library/onboarding/book) and on the owner's Mac
(~/Documents/racinglines/jb/book); build steps are in its README.md.
- [ ] BOOK-1 · put the first draft on the VM. The Mac build is clean (14 Oct 07:03 UTC, with the DITL #1
timeline fix). Run
bash scripts/deploy/vm.sh internal-docs ~/Documents/racinglines/jb/book/_build/htmlfrom a checkout ofmainif it has not been run yet (not verified from the thread), then openracinglines.bet/docs/internal/as an admin; anyone else should get a 404. - [ ] BOOK-2 · owner review of the draft. Every chapter carries a "Claude first draft (2026-10-14)" note; remove each note once the owner has read that chapter.
- [ ] BOOK-3 · DITL #1 follow-ups. (a) The owner decides whether the v4 joint prologue + GC paper slip goes
into DITL #1. (b) Only the book's copy of
ditl/core.mdhas the timeline fix (betting closed about 16:00 to 16:02 UTC, window missed by about 56 minutes, not 24); the Mac sourcecore.md, its timeline figure and the internal PDF still count to the 16:34 start. (c) Add the paper result once the Eneco GC settles (18 Oct). - [ ] BOOK-4 · missing sources. The runs/f1 matrix artifact was not saved;
research/algo-abclinksimg/pl-vs-global-win.png, which was not found (only its script exists); 5 of the 8 recovered 24-hour report figures are captures of the sign-in page; the Whistler, global-model sweep, Kalshi maker K and overnight sweep report texts are still missing (seesources/index.mdin the book).
Business and collaborators¶
- [ ] 2026 season report (December): live weekends by venue and profile against their replays, the
pricing scorecard for rounds 16–23, the championship sleeve, the disagreement log. Published under
reports/like the Whistler report. - [ ] Beta testers and collaborators: accounts and onboarding; contributor docs; a code-only public mirror or invited access to the private repo (it is private only because of the committed data set).
- [ ] B2B: selling the prediction models remains open.
- [ ] Trade on exchanges only after paper-trade validation (F1-8, the validation rules).
Done¶
Finished items, moved here from the sections above, with the dates they were done.
October 2026: the alpha, the recorders, the one engine (moved here 2026-10-14)¶
- [x] Fantasy league alpha on production (2026-10-12): sign-up and accounts (#146), the alpha layout with four tabs, five styles and F$1,000 sizing with Kelly (#160), settlement, leaderboard P&L and MCP trading tools (#161), the Lab tab opening on Follow a strategy (#166), the landing page and sign-in (#144, #167). Everything else is archived behind the layout switch (Alpha layout).
- [x] Recorders every 5 minutes, every day (owner rule 2026-10-07; #90, #91): Kalshi and OG.com books, trades every
15 minutes, Polymarket NASCAR and MotoGP books,
tape_check.shwarnings,backfill_tape.shfor gaps (Market tape). Forecast-refresh (#107) and schedule-sync (#169) timers. - [x] The one engine (2026-10-11, #128):
GlobalModelfor every modelled sport with three cores, prices identical to the old classes, which stay as challengers. - [x] Rounds 16 and 17 paper-traded on real markets on both venues; /term's Singapore parts (#152 to #157).
- [x] Racinglines 101 in three parts and the kart quickstart replay under
/docs(#139, #142). - [x] Sprint stage (U13's core, PR #73) and grid penalties (PR #74), merged 2026-10-06.
- [x] CLV for stored paper bets:
racinglines backtest clv(C4, #81); the coverage counterbacktest coverage(C0, #81);race_start_utcfor NASCAR (C42). - [x] Deploy guards: production and staging never share a venv (C57, #147); a deploy stops a long signals pass instead of refusing (#149); the smoke check follows the alpha layout (#164).
- [x] Data-terms review (owner, 2026-10-14): venue prices, MotoGP and
data pullmay reach signed-in users; the #aintcare warning stays. MotoGP back for the alpha,data pull(C51),racinglines remote(C53) (#179, promotion pending). - [x] Season demo database for staging (
racinglines_staging_demo, #173, #174): built on the VM 2026-10-13, every player on a random style through 2026;vm.sh staging db demo|mainswitches staging to it. - [x] Staging mirror code (#154, merged, not enabled); MCP results as drawn tables (#171).
- [x] Live trading history report (3 to 12 Oct, the project library) and the first DITL report (13 Oct).
Strategy session (2026-09-28)¶
- [x] U4 · Profile A's record vs the sweep, explained. The demo taker's record (+$575 in 2025, +$323 in
2026 on Polymarket) is about a third of profile A's (+$1,363 and +$1,243) because the demo taker
follows about a third of A's calls (
profiles.DEMO_FOLLOW0.33, see Following); taking every call makes +$2,606, the sum of the two. No fix needed. - [x] Maker record by venue checked: Polymarket +$285 (2025) and +$653 (2026), Kalshi +$193 and −$119, private book (Whistler) +$11,349.
- [x] Round 16's event name ("Bahrain Grand Prix in Malaysia") is correct: the race moved to Sepang.
Priorities (September)¶
- [x] Round 16 build: name aliases, the shared live core, the F1 adapter, the Live tab's F1 body, the demo taker, operations, a rehearsal on Baku (items B0–B8, cloud build-out 29 Sep).
- [x] A repeatable report command for any live event:
racinglines live report. - [x] Settle the Whistler private book in the database, into the demo maker's story.
- [x] A per-market loss cap and two-sided long-shot quotes in the shared quoting core (both off by default).
Done: Points validation¶
- [x] Store the tables in the
points_schemesdatabase table (per competition, era and round kind), and have the model read them from there:racinglines data points mtb_dh import, and--points dbonforecastandbacktest(default: the schema's placeholders). The entry filesports/points/uci_dhi_wc.tomlholds placeholders for every era until the official scales are in (2026-09-28, cloud build-out). - [x] A test that pins those totals (
tests/test_points.py): it checks every standings file insports/points/standings/and skips until the owner adds one.
Done: Data¶
- [x] Use UCI rider IDs (code, 2026-09-28, cloud build-out; synthetic tests, unverified against
the live API): the downloader writes a
UCI IDcolumn, the parser reads it (older files parse as before), and ingest matchesathlete_identifiers(scheme="uci")before names and reports a UCI ID shared by two athletes.racinglines db merge-athletes KEEP DROPmerges them. - [x] Re-download the downhill files (UCI IDs) and re-ingest (2026-09-28, locally; Data changes): 584 results moved onto the UCI-matched athlete, 1,090 UCI identifiers.
- [x] Merged 53 of the 58 reported pairs (2026-09-28). Five stay: both rows are in saved forecast run 1287 (Data changes).
- [x] 2021 PDFs (Leogang, Les Gets) and PDF-only Timed Training rounds: read by
download --pdf-results(backfill only) and ingested 2026-09-28 (775 results). - [x] Slug probing in the downloader (
--probe START END, 2026-09-28): all of 2021 finds exactly its six DH rounds. - [x] Canonical venue names (
mont-ste-anne→mont-sainte-anne,vallnord/vallnord-pal-arinsal→pal-arinsal): the database already stores every venue under its canonical slug (venue_aliases, checked on the snapshot: no duplicates);mtb_dh parse --canonical-venuesdoes the same for the CSV path (off by default) (2026-09-28, cloud). - [x] 2025 Polymarket F1 markets downloaded and synced (2026-09-27: 23 of 24 races have markets; minute prices and trades, no order books) and backtested in the cloud search.
Done: Model¶
- [x] Calibration check (2026-09-28, cloud build-out):
mtb_dh backtest --reliabilityover all 43 rounds. Win and podium are close; top 10 and make-Final are too flat. Cause: too much shrinkage (prior_n). A lowerINCIDENT_THRESHOLDand heavier-tailed ε (--eps-df) don't help (Calibration). - [x] New downhill defaults (owner's OK, 2026-09-28):
prior_n0.5, half-life 240 days, junior weight 0.25. Better in every market on the 43 rounds; picked in-sample. - [x] A tuning sweep over all 43 rounds (2026-09-28, cloud build-out):
prior_n0.5, half-life 240 days, junior weight 0.25 is better in every market (Calibration). Practice weight: flat between 0 and 1, 0.5 kept. - [x] Rider × venue effects: tried (2026-09-28, cloud build-out), no gain on the 43 rounds (Calibration); not built in.
- [x] Time trends within a season (2026-09-28, cloud build-out): a faster-improving-rookie drift is worse; none detected beyond what the half-life already tracks (Calibration).
Done: F1 model¶
- [x] Evaluation (F1-1):
racinglines model compare f1 RUN_A RUN_B(paired ± 2 SE, log loss, reliability bins) andracinglines model matrix f1; model-vs-Polymarket Brier for every market kind. - [x] Front of the grid (F1-2):
gridqcloses the gap to the grid-only baseline after qualifying. - [x] Pre-practice pricing (F1-2):
pretrain(a no-practice finishing model) closes the regression. - [x] Chaotic-race tail and correlated DNFs (F1-3):
tailis neutral at every stage; not promoted. - [x] Regulation reset:
resetdown-weights earlier seasons' car pace in a new-regulations season;gridq+pretrain+resetis the most accurate combination and profile A's model. - [x] Practice pace prior; car shared by teammates (2026-09-26).
- [x] Monte Carlo seed setting: sweep and profile setting
seed, unset = today's fixed seed (42), so noise replicates in a search are independent draws (2026-09-28, cloud build-out). - [x] Driver layer in a new season (F1-2): the
rookievariant fades a finished rookie season's teammate comparisons to a quarter. Small gain on win odds before qualifying; teammate head-to-heads better in 2026, worse in 2021 (F1 evaluation). Not promoted (2026-09-28, cloud build-out). - [x] Price fastest lap, safety car / red flag, rain (F1-3):
models/props.py,racinglines model props f1; opt-in kinds for a live book (2026-09-28, cloud). Per-circuit rates don't beat the field rate on 2022–2026 (F1 roadmap). - [x] Polymarket sync and repricing on a schedule during race weekends: the recorder re-syncs every 30 min; the signal engine prices each stage as its data arrives (every 5 min).
Done: Market making¶
- [x] As-of diagnostics, Polymarket minute prices and trade tape, maker replay, tests (Baku).
- [x] Keep
markets recordrunning through every race weekend (LaunchAgent, F1-0). - [x] Fetch trades after each session: the signal engine pulls prices and trades for the weekend's markets on every run.
- [x] Maker options (F1-4): flatten before qualifying, info-timed skew, markout-driven widening. None beats the default maker.
- [x] Stage-aware taker (F1-4): in the sweep (+$1,874 in-sample on 2026).
- [x] Stage-aware taker out of sample on 2025: not confirmed (−$316 on 23 weekends, against +$1,874 in-sample on 2026; Market making).
- [x] Replay across every race with a tape (2025 and 2026), then tune: the
params-4hcloud search over 1,161 settings combos, judged on both seasons. - [x] Live watch list: the Markets page lists every open Polymarket F1 market with the profile's call and heat; new markets raise alerts (macOS, ntfy, webhook, log).
- [x] Migrate to CLOB V2 (found in F1-0):
markets/polymarket/trade.pysigns withpy-clob-client-v2==1.2.0; dry runs sign locally (V2 struct, EIP-712 domain 2) and the tests recover the signer (2026-09-28, cloud build-out).
Done: Paper trading¶
- [x] Bankroll-aware sizing and a deployed-capital cap in the backtest: sweep settings
bankrollandmax_deployed, unset by default (2026-09-28, cloud build-out; synthetic tests). - [x] Fix: an h2h-only market-kinds sweep crashed on 2025 (
KeyError: 'cond'in the maker replay's summary on a weekend with no market of the chosen kinds; 2025 lists no h2h before round 8). An empty weekend now summarises to zero (2026-09-28, cloud build-out).
Done: Live events¶
- [x] Live tab for both demo users; downhill finals from UCI timing, rank probabilities, maker quotes, a simulated private book (1,000 anonymous takers with event budgets), P&L by venue, replay logs.
- [x] Replay on the Live tab once an event is over (timeline, step, play), with the book rebuilt from the logged fills; P&L by venue on Positions (2026-09-27).
- [x] A report command for any live event (the Whistler report, made repeatable): P&L by update, the
crowd, the demo taker, the fair-price scorecard (
racinglines live report <spec> --pdf, 2026-09-29). - [x] Re-run an event's pricing from its logged raw feed:
racinglines live reprice <spec>(downhill; every raw response re-priced with today's code and the recorded inputs, scored against what was quoted live; 2026-09-29). On 38 sampled Whistler updates the re-price (v2 throughout) scored a little worse than the live quotes (v1, then v2): Brier 0.0216 vs 0.0202 on win, 0.0430 vs 0.0365 on podium. - [x] Two-sided long-shot quotes (or a per-market loss cap) so the maker doesn't pile up shorts in unlikely
winners:
[live.quoting] max_lossandfloor_bidinmarkets/quoting.py(2026-09-29), both off by default. Measured on Baku's rehearsal in the decision log. - [x] Settle the Whistler private book in the database and add it to the demo maker's story: the
live_eventstable (migrationa3f5c8d1e7b2) andracinglines live settle <spec>; the story dates private-book events from it (2026-09-29). Locally:alembic upgrade head, thenracinglines live settle live/mtb_dh/20260925_mtb.toml. The final's results still come from the usual downhill download and ingest (the event isin_progressin the snapshot).
Done: Exchanges¶
- [x] Kalshi connector (
racinglines/markets/kalshi/): markets (with their resolution rules), prices, trade tape, price history, order books, and post-only orders behindKALSHI_TRADING_ENABLED(racinglines markets --exchange kalshi …). Built on mocked responses only (2026-09-28, cloud): the cloud network blocks Kalshi's API.venue_replay.Kalshiis its backtest venue; the Polymarket paths (sweep, maker replay, season strategy, head-to-head pairs, links export, fetches) now readexchange = 'polymarket'only, so synced Kalshi links can't leak into them. - [x] Kalshi against the live read-only API (2026-09-28, locally):
sync --closed, trades, history and books run into a copy of the database. Fixed: series found by ticker (KXF1*; "Qualify in Pole Position" doesn't say F1), sprints unmodeled (the sprint winner and sprint pole were read as the race's), top 10 →race_top10, head-to-heads ("Will A beat B in the racing matchup?") with the race from the ticker's code,volume_fp,orderbook_fp(books came back empty), dollar strings in historical candlesticks (read as cents), and Kalshi's/historicalendpoints for markets settled before its cutoff (older events came back with no markets). 2026: 15 weekends, 1,847 modeled links. - [x] Whistler replay (2026-09-28):
PrivateBook.from_runnow re-derives Whistler's recorded book fill for fill. Two causes, both recorded in the run folder'sreplay.json(Live events): the crowd's rate rule changed mid-event, and two overlapping loops logged polls in the same second. Polls now log their rate. The test had always skipped (its module fixture pointedpaths.DATAat a temp folder). - [x] Kalshi in the maker replay and the demo maker's record (
f1 demo-history --venue kalshi, PR #10): per-ticker grouping, Kalshi's maker fee,RACINGLINES_KALSHI_VENUE=1for the board / race / Positions pages. Feed differences vs Polymarket: Kalshi history. - [x] Kalshi: the first real pull (2025–26, owner's device) replayed and summarised: how many of the maker's
quotes Kalshi's tape fills, by market kind (Kalshi history);
Kalshi at parity with Polymarket in the app, on by default (
RACINGLINES_KALSHI_VENUE=0hides it): Positions tiles and curve, Strategy switch,/markets/kalshi, race-page column and chart, Lab replay (Kalshi history).
Done: Engineering¶
- [x] Web app (
racinglines/web/): predictions, histories, backtests, Polymarket maker orders. - [x] Pages and routes that match their names (Markets, Strategy, Positions, My Book, Lab); old URLs redirect.
- [x] Demo accounts with a backtest-replay track record, disposable demo sessions, interaction logging.
- [x] One settings schema for sweeps, the Lab and cloud searches; cloud searches from a database snapshot (an exact replica).
- [x] Pinned
requirements.txt(pipeline) andrequirements-docs.txt(mkdocs). - [x]
pyproject.toml:pip install -e .installs theracinglinescommand. - [x]
scripts/build_readme.py --checkruns in the pre-push hook, withmkdocs build --strict. - [x] PostgreSQL database instead of re-parsing files every run.
- [x]
.gitignore:data/stays ignored except the allow-listed minimal set for cloud runs. - [x] Admin overview: the takers' P&L leaves out the
polymarket-takersreplay counterparty and shows it on its own line (2026-09-28, cloud build-out). - [x] JSON API endpoints next to the pages: read-only events and athletes, behind the login,
off unless
RACINGLINES_JSON_API=1(JSON API, 2026-09-28, cloud).