F1 roadmap¶
A phased plan for extending the F1 model and market-making tools, written for a coding agent and the people reviewing its work. The concepts and links behind each phase are in the F1 reference. What already exists is documented in Formula 1, Market making and Testing.
Phases are named F1-0 … F1-9 so they don't collide with the platform phases in Project history (data layout, one package, sport schemas). Each phase starts only after the owner confirms it.
Priorities across everything, F1 phases included, are on the Roadmap. The top priority this week is the F1 live test (round 16, 2–4 Oct 2026), part of F1-8.
Ground rules¶
1. The baseline stays in place¶
The current position_sim model is the baseline and stays the default until a
challenger beats it under the promotion rule below. Since the one engine (staging, 2026-10-11; decision log
"One engine") it runs as the laps core of racinglines/models/model_global.py, prices unchanged and runs still
stored as f1_sector_sim; the PositionSim challenger was retired on 2026-10-19 (decision log)
(One engine).
- Every new behavior ships behind a switch that defaults to today's behavior. With the default, every existing stage produces the same output, and the existing golden files still pass untouched.
- Challengers plug into the existing pricing path. Anything that prices a
race goes through
inputs.laps_pricing.price_raceand reads data throughMeasurements.view(cutoff), so it inherits the as-of cutoff andassert_no_leakfor free. No second pricing path. - Nothing is renamed or moved. No existing module, function, CLI command, table or config value changes name or location.
2. No arbitrary code changes¶
A change belongs in a phase only if it does one of these:
- closes a measured gap (listed in the reference);
- is required by a phase's deliverable;
- fixes something that is broken.
Refactors "while you're in there" are out of scope. If one seems necessary, add it to the Decision Log with the reason and wait for approval.
3. Structural changes: the complete list¶
These are the only structural changes this roadmap makes. Anything else needs a Decision Log entry and approval first.
| Change | F1 phase | Why it's needed |
|---|---|---|
Add f1-roadmap.md and f1-reference.md to mkdocs.yml nav |
0 | The docs site is how docs are read; two nav lines under Background, no other doc moves |
A racinglines model compare f1 RUN_A RUN_B command |
1 | The paired ± 2 SE tables are the promotion rule's evidence; until now they came from one-off scripts |
Sport schemas sports/*.yaml, read by existing code |
platform (between 0 and 1) | The owner's goal that sport differences live in configuration. Additive only: nothing moves or is renamed, and every golden file stays unchanged |
| Optional dependency extras for LightGBM (and later PyMC/NumPyro) | 2, 6 | Keeps racinglines check and the regression suite runnable without heavy packages. Not added to the pinned requirements.txt unless a challenger is promoted |
A finishing-model switch inside position_sim (default: the current ridge) |
2 | A challenger must use the same inputs, simulation and pricing to be a fair comparison and to inherit the leakage guards |
New files inside existing packages (e.g. models/inputs/finishing_gbm.py) |
2–4 | New code sits next to the code it extends; no new top-level packages |
New model family racinglines/models/rank_logit/ |
6 (optional) | A different kind of model (fit on finishing orders, sampled with Gumbel-max) that doesn't belong inside position_sim. It must feed the same pricing path; if no clean seam exists, propose the smallest one in the Decision Log before coding |
New venue package racinglines/markets/kalshi/ |
9 (was 5, optional) | Kalshi lists F1. Mirrors markets/polymarket/ and registers in markets/venues; order placement behind a flag, off by default |
A settings schema (pipelines/sweep_settings.py), strategy profiles (pipelines/profiles.py) and the signal engine (pipelines/signals.py, racinglines strategy signals f1) with tables strategy_signals and paper_positions |
8 | Paper trading must run the backtest's own code on live data, and any combo found must be recreatable. New files and tables only; defaults reproduce the old sweep exactly |
New source racinglines/sources/openf1/ |
7 (optional) | Only for in-race trading, which needs a live feed; FastF1's archive arrives after sessions end |
4. Tests must keep passing¶
Before starting, build the fixtures and write your local golden baseline:
python -m pytest -m "not live" # fixtures and goldens are pinned in git (scripts/fetch_test_fixtures.py --refresh rebuilds them)
For every change:
- Run
racinglines check. Then runpython -m pytest -m "not live"; it must pass against the unchanged golden files. Runpython -m pytesttoo when the live database is available. - Put new behavior in new tests with their own golden files
(
tests/golden/<new_stage>.json), built on the existing fixtures where possible. - If new fixture data is needed, add a new flag to
scripts/fetch_test_fixtures.py. Never change what--f1or--mtbproduce. - Tests that need an optional package start with
pytest.importorskip(...), so the suite still passes without it. - Extend
racinglines/testing/synthetic.pyadditively, so the quick check also covers new code where that's practical. - Every new pricing or trading path gets a leakage rule test: inject data
after the cutoff and expect
LeakageError, or check that the quote doesn't change (thePublicViewpattern intests/test_replay.py). tests/test_no_data_in_git.pymust keep passing. Fixtures and golden files stay local.
UPDATE_GOLDEN=1 is allowed only in a promotion change (below). The stages
that change must be exactly the ones downstream of what was changed, as in the
table at the end of Testing.
5. Promotion rule: when a challenger becomes the default¶
All of these, in one change:
- A full backtest (
racinglines model backtest f1 --save) of challenger vs baseline over every race, with paired differences ± 2 standard errors at each pricing stage. - Better on the target metric by more than 2 SE, and no market type worse by more than 2 SE at any stage.
- The season sweep re-run with the challenger (pricing is cached, so this is quick).
- Results tables added to Formula 1 in the existing style (differences are new − old, negative is better).
- A Decision Log entry.
UPDATE_GOLDEN=1 python -m pytestin the same change, with the changed stages listed in the commit message.
Market-making settings follow the project's own rule: no tuning on one event. Tune only after the replay has run across every race with a recorded tape.
6. Keeping the docs current¶
- Built features are documented where they already live: Formula 1 for the model, Market making for trading, CLI reference for new commands or flags. Match the existing style: short sentences, tables, ± 2 SE.
- The Roadmap is the task list and sets the priorities. Phases below point at its items rather than copying them. Tick items there when done, and add new ones there.
- This file: update each phase's Status, and add Decision Log rows. Never delete a row; add a new one that supersedes it.
- F1 reference: when work changes the understanding behind an idea (a knob mattered, a strategy didn't work), update the section and its changelog.
- Project history: add a line to history when something ships, following that page's existing format.
- README: if headline numbers change, run
python scripts/build_readme.py.
Phases¶
Phases are ordered by the measured gaps they close. Each one lists its target gap, what it changes, and what "done" means.
F1-0: Docs integration and baseline check¶
Status: done 2026-09-26 · Behavior change: none
- ~~Add both pages to the
mkdocs.ymlnav.~~ Done: under Background. The pre-push hook (scripts/hooks/pre-push) runsmkdocs build --strict. - ~~Build fixtures and write the local golden baseline.~~ Done: 20 golden files;
racinglines check21/21,pytest118 passed. - ~~Record the reference runs in the Decision Log.~~ Done: backtest run 115, sweep run 191, season strategy run 113.
- ~~Read-only check: does
racinglines/markets/polymarket/tradesign CLOB V2 orders?~~ No. It uses the V1py-clob-client(0.34.6). Polymarket's changelog says V1 SDKs and V1-signed orders stopped working on 2026-04-28, and Python code must move topy-clob-client-v2. Logged as a separate fix (see the Decision Log and Roadmap). Nothing is at risk meanwhile: trading is off by default, and read-only market data doesn't use the client. - ~~Operations: make
markets recordrun through every race weekend.~~ Done: it runs as a LaunchAgent (bet.racinglines.recorder, see CLI) that starts at login and restarts on exit. F1-4 depends on this recorded book depth.
Done when: docs build, suite green, reference runs logged.
F1-1: Evaluation additions (report-only)¶
Status: done 2026-09-27 · Target: better measurement for every later phase
Outcome: racinglines model compare f1 RUN_A RUN_B [--reliability] prints the paired
± 2 SE tables (Brier and log loss, which the backtest already recorded) and
calibration bins. Backtests now keep each driver's probabilities for this.
The sweep scores model vs Polymarket for every market kind. racinglines model matrix f1 summarizes every variant × strategy. No existing golden file changed.
- Add log loss and reliability curves to the F1 backtest report, per market type and pricing stage.
- Model-vs-Polymarket Brier per stage: the season sweep already reports it for win and pole over 2026 (sweep run 191). Extend it to every market type.
- Add
racinglines model compare f1 RUN_A RUN_B: the paired ± 2 SE tables used in Formula 1. The Lab has no such comparison (the tables so far came from one-off scripts); it can show the command's output later. - Keep the new metrics out of existing golden fingerprints. Write them as separate outputs with their own tests, so no existing golden file changes.
Done when: one command produces the comparison tables for two saved runs.
F1-2: Finishing-model challengers (reference model A)¶
Status: done 2026-09-27; nothing promoted (owner: iterate first). Paper-trading profile A uses gridq+pretrain+reset through its settings · Target: gap 1 (win after qualifying loses to grid-only) and gap 2 (pre-practice regression)
Outcome (tables in Formula 1):
- step 1,
grid: closes gap 1 after qualifying but is worse before it;gridq(the term only once the grid is known) keeps the gain with no loss; - step 2,
pretrain: closes gap 2; - step 3,
gbm(run 665): worse than baseline (teammate h2h at every stage; podium and win log loss before and after qualifying), so not promoted; gridq + pretrainmeets the backtest half of the promotion rule;- follow-up,
reset(run 837): better in new-regulations seasons, including 2022;gridq + pretrain + reset(run 854) is the most accurate combination. Not promoted (owner: iterate first). - The
params-4hcloud search (2025 and 2026) confirmedgridq+pretrain+resetas the taker's model (profile A) and foundgbmbest for the conservative maker (profile C), althoughgbmloses on accuracy.
Steps, cheapest first:
- Nonlinear grid term in the ridge (TODO: "grid-position prior"): grid buckets or a spline, behind a config flag.
- Stage-specific training (TODO: pre-practice got worse): train the finishing model on no-practice paces for pre-FP1 pricing, behind a flag.
- GBM finishing model (
models/inputs/finishing_gbm.py, LightGBM as an optional extra), selected by the finishing-model switch. Same inputs as the ridge, monotone constraints on grid and pace gaps, and σ from out-of-time residuals so the simulation's spread stays comparable.
Each step is evaluated with F1-1's comparison. Promote a step only under the promotion rule. Step 3 is skipped if steps 1–2 close gap 1.
Done when: each step has a comparison table in Formula 1 and a Decision Log entry (promoted or not).
F1-3: Simulation tail and props (reference model C1)¶
Status: model part done 2026-09-27; props priced for the live book 2026-09-28 (opt-in, models/props.py), not yet mapped in the Polymarket sync · Target: gaps 3 and 4, plus unpriced prop markets
Outcome: tail (disrupted-race mixture + correlated retirements) is neutral:
no metric moves beyond 2 SE at any stage, so it isn't promoted. Props are
deferred (Decision Log).
All inside the existing simulator, each behind its own switch (default off):
- Chaotic-race mixture: per-circuit
p_chaosfromlapstrack status and the weather inrounds.extra, shrunk to the field average; inflated noise and DNF rates in chaotic runs. - Correlated DNFs: a shared team DNF draw sized to the measured +0.11.
- Prop pricing: safety car, red flag and rain probabilities per circuit from the same history. Add new prediction kinds and map them in the Polymarket sync's classifier.
Done when: comparison tables for top-scoring constructor, top 10 and head-to-head, plus a calibration check for the props.
F1-4: Maker inventory and timing (reference strategies 1 and 3)¶
Status: done 2026-09-27, except the queue model, liquidity rewards and Kelly caps; tuned in the params-4h search · Target: gap 5 (pre-qualifying inventory, adverse selection)
Outcome: the maker options and the stage-aware taker are in the sweep, with rule tests. Results are in the model × strategy matrix. None of the maker options beats the default maker, and the stage-aware taker's gain is in-sample.
Everything goes into the existing maker replay as options. Today's settings
stay the default, and tests/test_replay.py keeps passing unchanged.
- Queue model on recorded book snapshots (TODO), as a third fill rule next to touch and through.
- Inventory skew with an Avellaneda-Stoikov-style option: skew grows with time to the next information event, not just to resolution.
- Pre-qualifying flatten or hedge (TODO), with a configurable target.
- ~~Pull windows for every session type.~~ Already the case: each replay stage ends at the next session's start (practice included), and quotes are pulled 15 min before it.
- Markout-driven widening: use
markout_60mper market type and stage as an input, not just a report. - Polymarket liquidity rewards in replay P&L as a separate line.
- Optional: fractional-Kelly caps as an alternative to fixed share limits.
- Stage-aware taker (reference strategy 3, for the weekend taker): stop re-trading after FP3 and qualifying. In sweep run 191, trades after FP1, FP2 and the sprint made +$1,944, while trades after FP3 and qualifying lost −$2,353. Judge it on events after Baku, not by tuning on these.
Precondition for tuning: the replay has run across every race with a
recorded tape. Met 2026-09-27: every 2025 and 2026 weekend with a Polymarket
tape, searched over 1,161 settings combos in the cloud (params-4h, see
Cloud sweeps); tuned on 2026, confirmed on 2025. The
queue model waits for recorded book depth (F1-8 collects it).
Done when: each option has rule tests (in the style of test_replay.py) and
a sweep comparison against the current defaults.
F1-5: Linked-market quoting (reference strategy 2)¶
Status: deferred (unchanged 2026-09-27) · Target: the "re-quote linked markets" TODO
Why deferred: the model's prices are coherent by construction (one
simulation), and between pricing stages our fair values don't move, so "re-quote
when one book moves" would mean pricing from market data, which the project
rules out. What's left is hedging a fill in a more liquid linked market, which
needs order-book depth to replay honestly: recorded from 2026-09-26 on
(markets record). Revisit once several weekends of books are recorded.
- Coherence checks across win / podium / head-to-head / constructor from the simulation's joint output, reported alongside the existing group-sum check.
- In the replay: re-quote all linked markets when the model or one book moves; optional hedge into the more liquid leg with a leg-risk timeout.
- Optional second venue: only after confirming another venue lists F1 with real depth and compatible resolution rules. Store resolution rules per market and refuse to treat markets as linked if they differ.
Done when: replay comparison shows whether linked re-quoting helps, across all recorded tapes.
F1-6 (optional): Plackett-Luce challenger (reference model B)¶
Status: started 2026-10-14, narrowed and provisional (decision log 2026-10-14, branch track/models-pl) · Core v2 stage 1 (covariates and stages) on PR 295 (draft, off by default) · Core v2 stage 2 (place weighting, walk-forward temperature, the pole market, grid penalties, dnf_team; the walk_forward job's settings and stage) on PR 296 (draft, off by default) · Core v2 stage 4 (grid_sim, the split temperature, track-feature and driver-in-car effects, quasi-SEs, the calibration hook) on PR 299 (draft, off by default) · Start only if F1-2 and F1-3 leave a gap, or per-market
uncertainty is wanted for maker spreads
racinglines/models/rank_logit/: one Davidson-Luce model in NumPy/SciPy (MAP by L-BFGS, analytic gradient); PyMC or NumPyro declined (decision log 2026-10-14).- Output feeds the same pricing path (see the structural-changes table).
- Judged as a challenger under the promotion rule, or used only as a per-market uncertainty input for F1-4 spreads.
rank_logit¶
racinglines backtest walk-forward <sport> --model rank_logit (F1, NASCAR, MotoGP, downhill; also the Lab and MCP
walk-forward job's model). It is the results core (models/inputs/results.py: loading, start lists, the
days-before-the-event leakage rule, settlement and the same retirement draw) with a Davidson-Luce fit in place of
the probit strength, refit per event and drawn by Gumbel-max. [model.rank_logit.rankings] in each schema lists the
round kinds that may become rankings and their weights (the first with a positive weight is the kind priced: race,
downhill's final); ranking_weights (race=1,qual=1) overrides them per run. Each past round is one ranking weighted
by its kind's weight times 0.5^(age / half_life_days): classified entrants in order ([results] classified), the
other starters an unordered tail. tie_on = "time_ms" ties identical classified times (max_tie caps the tie size),
top_k keeps the first k places and moves the rest to the tail, team_effects adds a team log-worth ([model]
group), kind_sd an athlete × round-kind offset (which also prices race_pole from qualifying strengths), and
sd_athlete, sd_team, delta_prior_mean and delta_prior_sd are the normal priors. cov computes the Laplace
covariance and sd_scale draws parameter uncertainty from it into every simulation. The defaults (race rankings,
no ties, teams, truncation, kind offsets or covariance) are the 6–7 Oct fits; none is tuned.
Covariates and stages (Core v2 stage 1, PR 295, draft, off by default). The log-worth is
θ_i + φ_team + κ_kind + Σ_c β_c · x_c(i, e, s): β is one vector fitted with θ on the history's race rankings (prior
N(0, beta_sd²)), each past race carrying the covariates it had, and the event priced reads only the sessions run by
its stage. F1 uses an as-of session view at the exact stage cutoff while retaining the existing event-day base-fit
rule; its grid and qual_gap come only from qualifying, never the race result row. A schema declares what its sport
can provide, [model.rank_logit] covariates, each tied to an input it declares: grid (the starting grid; the event
priced: its qualifying position), qual_gap (seconds to pole, ≤ 3),
practice_pace (best practice lap to the session's best, seconds, ≤ 3, the mean over the [sessions] practice rounds
run) and sprint (the sprint's finishing position); positions enter as −log(position), gaps as −gap, each centred over
the field (the gaps since stage 2), so an unknown value (0) is the field's average, not pole. F1 declares these
covariates plus grid_front and the opt-in no_timeflag; NASCAR declares grid and qual_gap (its inputs have no practice), MotoGP
grid, qual_gap and sprint, downhill none. The stage comes from Event.info["sessions"], as the season sweep's
"sessions" mode supplies it, or backtest walk-forward --stage LABEL (a label of the schema's [stages], e.g.
after Quali; core/stages.py known_at): pre-weekend every covariate is unknown, after practice practice_pace, after
qualifying grid and qual_gap, after the sprint sprint; F1 inputs are limited to sessions ended before the stage
cutoff. A selected covariate whose source session is known but has no usable data raises rather than silently becoming
the field average. Before qualifying, selected F1 grid/grid_front/qual_gap raise; the explicit
grid_sim=true option is the exception for grid covariates, drawing a qualifying order per simulation, while qual_gap
cannot be simulated. Event 11852's historical after-Quali label used event-day midnight as its covariate cutoff;
11864's same label used race-start minus one minute. The event-day base-fit rule remains unchanged. Settings:
covariates (a comma list of the declared names; unset = none) and beta_sd (unset = 1.0); both unset leave every
settings key, draw and price as before. qual_gap_no_time is an explicit, default-off F1 option requiring qual_gap:
a classified qualifier with a valid position but no measured time receives the slowest measured gap in that same
qualifying session and a fitted no_timeflag indicator. The event diagnostics report the number assigned and the
setting is included in run settings/tags. If that session has no measured qualifying time, pricing raises; a missing
qualifying row or position is never treated as a no-time classified result. Without the option, missing qualifying
times raise as before. This is not enabled or tuned by default. Not run on real data yet (the local database was down);
none is tuned.
Sharpness and the pole market (Core v2 stage 2, PR 296, draft, off by default). Every setting below is unset by
default and stays out of the settings key; unset, every key, draw and price is as on PR 295 (the PIN_WIN pin and the
key of every schema are tested). None is tuned.
place_decay(places): the likelihood term of the s-th block chosen weighs exp(−(s − 1) /place_decay), a softtop_kthat keeps the back of the field in the fit; withtop_ktoo,top_ktruncates first and the decay applies to what is left.temperature: after the fit, the priced field's log-worths (and the side orders') are multiplied by τ before the Gumbel draw; above 1 the favourite is sharper. A number is a fixed τ (0.1–10).fitpicks τ in [0.5, 3] that maximises the Plackett-Luce likelihood of the lasttemperature_window(unset 40) races dated before the event, each under its own as-of worths: the fit on results before that race's day, its covariates read at the same stage as the event priced (at_stageof that race; a race without that stage is left out). Identity until 8 races in that window have usable out-of-sample worths. Raw and fitted-temperature arms retain the same event-date history rule for base fits, retirement/side histories and temperature history. This is not a settlement-time gate orMeasurements.view(cutoff); session-end backfill and settlement gating are a separate reviewed todo.temperature_top_kfits τ on the first k places only. A single τ is refitted per sport and stage, not per market, and recorded per event inwalk_forward_events.csv(columntemperature). Tests flip future results and the priced event's race/sprint: neither can feed its own fit or move the price, including MotoGP before its sprint.- The pole market: with
ranking_weightsturningqualon,stage_rank["qual"]is drawn from the qual-kind worths (θ + the qual offset withkind_sd+ β ·practice_pacewhere selected;gridandqual_gapare not known before qualifying). Without ties it is the Gumbel-max draw; withtie_ongiving ties (NASCAR qualifying) it is exact sequential Davidson-Luce with the fitted δ, and a tied block takes its places in a random order. The stored classification gives every tied time a distinct position (NASCAR on staging: 174 tied pairs, none sharing one) andrace_polesettles onqual_position == 1, so one entrant is YES. F1 declares no[model] sessions: its pole is drawn inside rank_logit and settled from the qual round, withsports/f1.tomland the results core's side path untouched. - Grid penalties: where the schema declares the
gridinput (F1), the event priced has its qualifying order moved by the confirmed penalties ofinputs/grid.py(as of the stage's latest session start) before it becomes thegridcovariate. NASCAR and MotoGP declare nogridinput; their grid is the qualifying order. dnf_team(pseudo-starts): the retirement hazard is the results core'sdnf_rate(decayed non-finishes over decayed starts, shrunk toward the field's by 5 pseudo-starts), also shrunk toward the team's shrunk rate ([model] group).- Every surface: the walk_forward job (Lab, MCP
run_job) takesstage(a[stages]label) andsettings(the chosen model's settings, validated against its schema; in the Labname=value; ...), passed tobacktest walk-forward.
The front of the grid and staged baselines (Core v2 S3b, draft, off by default). After the stage-3 pass test (F1 2024–26: the walk-forward τ fitted on every place comes out below 1 in every event while the pole-sitter is underpriced), two additions, neither tuned:
grid_front(covariate, F1 declares it): the same grid asgrid(moved by confirmed penalties when priced) as two 0/1 indicators, grid 1 and grid 2–3, each centred over the field and each with its own β (two columns of x), since −log(grid) under-weights the front-row premium. Unset (not named incovariates), every key and price is unchanged. Kept as an off-by-default diagnostic: on F1 2024–26 its fitted βs are negative next to −log(grid) (P1 median −0.93 / −0.53 / −0.41 withtop_k5), so it is not a sharpness lever; the lever is place weighting (place_decay,top_k). The fitted βs (one per column of x) are written to the walk-forward's events rows (beta_<column>).- The engine (
GlobalModel, every core: results, laps, timed runs) has an identityat_stage: it prices an event the same at every stage label, sobacktest walk-forward --stage LABELruns for--model globaland for the schema's own engine (the F1 laps core) and their records carry the label, pairing with a stage-pricedrank_logitrun ineval compare. It is a method, not a setting (no settings key); its surfaces are the CLI's--stageand the walk_forward job'sstage(Lab, MCPrun_job).
The robust variant (Core v2 stage 4, PR 299, draft, off by default). The owner's references (the PlackettLuce package,
Turner, van Etten, Firth and Kosmidis, arXiv 1810.12068) mapped onto the one likelihood, each a setting or schema key
that is unset by default and stays out of the settings key: unset, every key (106acb09a884 for every schema,
5fe30a4d2ec9 with sims=1000, seed=1, top_k=5) and the PIN_WIN prices are as on PR 295 (tested per item). None is
tuned.
| Setting / key | Unset | What it does |
|---|---|---|
grid_sim (bool) |
off (false counts as unset) |
before qualifying, each simulation draws a qualifying order and prices the race from that grid; the pole market is the same draws |
temperature_front (number or fit) |
off | split temperature: the first temperature_front_k entrants by worth at τ_front, the rest at τ |
temperature_front_k (int) |
3 | how many entrants the front temperature covers |
[model.rank_logit] track_features (schema) |
none declared but F1's | the per-event features track_sd reads; F1: x_track, ease, street |
track_sd (float) |
off | athlete × track-feature offsets γ, prior N(0, track_sd²) |
pair_sd (float) |
off | an offset per (athlete, team) pair, prior N(0, pair_sd²) (needs [model] group) |
cov (existing) |
off | now also writes each entrant's quasi-SE to the events rows |
calib_power (alias calib; walk_forward or a number) |
off | the engine's walk-forward win-price calibration on rank_logit's draws |
- Most probable grid through the week (
grid_sim). Before qualifying has run (pre-weekend, after any practice session), with a grid covariate selected (gridorgrid_front), each simulation first draws a qualifying order from the qual-kind worths exactly as the pole market does (θ + the qual offset + β ·practice_paceof the sessions run, with τ; ties pertie_on) on its own side generator, moves it by the confirmed grid penalties, and adds β · x(that grid) to its race worths (qual_gapstays unknown: no times exist yet). So every race price is the average over simulated grids, it moves as practice arrives (withpractice_paceselected), and the pole market (stage_rank["qual"]) is the same draws, before penalties. Once qualifying has run the real grid is used, exactly as without it (tested: identical draws). F1 2024–26, seed 42, pre-weekend, the Mexico string plus--grid-sim(run 1534): win log loss −0.0069 ± 0.0043 againstglobal(run 1498; 2025 −0.0113, 2026 −0.0074), −0.0009 ± 0.0023 against T3PD4 (run 1502) and −0.0030 ± 0.0036 against the same string without it (run 1521); podium −0.0216 againstglobal, −0.0064 against run 1521; head-to-head and top 10 worse than both T3PD4 and run 1521 (+0.0066, +0.0081 / +0.0095); the pole market unchanged (+0.0021 ± 0.0024 against T3PD4). The pole-sitter (won 0.615 of 65; 0.706 of 17 in 2026) is priced 0.211 (2026: 0.120), against 0.161 byglobaland 0.191 by T3PD4. One run, one seed: a lead, not a choice. - Split temperature (
temperature_front,temperature_front_k). One τ fitted on every place comes out below 1 while the front is underpriced (S3). The split temperature scales the log-worth gaps among the first k entrants by worth by τ_front and everything else by τ (temperature, else 1): τ·w + (τ_front − τ)·max(w − w_(k), 0), a monotone map (likelihood.temper) applied wherever τ is (the race, the side orders, the qualifying draws). A number fixes it;fitfits it walk-forward on the same objective astemperature(the lasttemperature_windowraces before the event under their own as-of worths, at the event's stage), jointly with τ whentemperatureisfittoo, else with τ held, bounded [0.5, 3]; recorded per event (walk_forward_events.csvcolumntemperature_front). - Track-feature effects (
track_sd; the within-model alternative topltree). A schema names the per-event features its sport supplies,[model.rank_logit] track_features, each tied to a declared input. F1 declaresx_track(the venue's fast/slow-sector speed profile),ease(its mean places gained / 2.8) andstreet(1 on[tracks] street), all from inputsectors: the laps core's track profiles (inputs/laps.pyvenue_track_features, the venue's dry profiles ready before the event's first day; 154 F1 events have all three in the local database). Every ranking carries its event's features, standardised over the rankings of each fit, and every athlete's log-worth adds γ_athlete · features with γ ~ N(0,track_sd²), fitted jointly; the event priced uses its own features standardised the same way. NASCAR, MotoGP and downhill declare none (track_sdis refused there). - Driver-in-car pair (
pair_sd). Every entry carries its team ([model] group) and each (athlete, team) pair seen gets an offset ψ ~ N(0,pair_sd²) beside θ (and φ withteam_effects), fitted jointly; an unseen pair is at 0. Withcovandsd_scalethe parameter draws include ψ (γ is left out of them). - Quasi-standard errors. With
covon, each event's row holds every entrant's quasi-SE of its log-worth (JSON {athlete id: SE}, columnquasi_se, also in the saved metrics): Firth's quasi-variances of the Laplace covariance's contrasts (qvcalc: q_i + q_j ≈ var(θ_i − θ_j), minimising Σ (log(q_i + q_j) − log V_ij)²), so any two entrants compare without the full covariance. They cover the member parameters (θ, φ, κ, ψ), not β · x or γ. - Calibration hook (
calib_power, aliascalib). The results core's own setting, reused: the same walk-forward power calibrationmodels/calibration.pyapplies to the F1 engine (win prices p^a / Σ p^a by resampling the drawn races on a side generator, so podium, top 10 and head-to-head follow).walk_forwardreadssports/<sport>/calibration.toml(F1 and NASCAR have one; F1's is fitted on the laps core's prices), thegridpower once qualifying has run at the stage andnogridbefore; its table's hash is in the key. The side classifications (the pole market, alsogrid_sim's) are not resampled, as in the results core. This gives a calibrated rank_logit row beside the calibratedglobal. - Pseudo-rankings and the normal prior. The package's pseudo-rankings (
npseudo: each item wins and loses one comparison against a hypothetical average item) keep a sparse or disconnected comparison graph estimable; rank_logit's normal prior on log-worths (sd_athlete) does the same job: it ties every athlete to the prior mean 0, so every log-worth is finite and an unseen entrant sits at the average. The two are equivalent in effect, not identical (a pseudo win and loss is a logistic prior of fixed strength;sd_athletesets the strength). - Every surface: each setting is in rank_logit's
Settingsschema, sobacktest walk-forwardhas the flag (a bool flag alone means true:--grid-sim), and the walk_forward job'ssettings(Lab, MCPrun_job) take it.
First comparison (2026-10-14, F1 only; report local-only at reports/2026-10-14-rank-logit-walk-forward/, run ids 1450–1468).
Walk-forward F1 2024–2026 at the results level (--model global as the baseline, seed 42, 2,000 sims, 65 events, 16,713
paired fair values per comparison through eval compare; target 2026, held out 2024 and 2025). Log loss, diff against
global ± 2 SE over events (* = interval excludes 0; negative favours rank_logit):
| rank_logit setting | Win 2026 | Win 2024 | Win 2025 | Win, all | Head-to-head, all | Label |
|---|---|---|---|---|---|---|
| defaults (the 6–7 Oct fit) | +0.0143±0.0054* | +0.0084±0.0049* | +0.0093±0.0027* | +0.0103±0.0025* | +0.0079±0.0036* | not better (the 6 Oct win loss reproduces: 0.1572 vs 0.1468) |
tie_on = "time_ms" |
identical to the defaults: F1 2023–26 race results have no tied classified times | no-op in F1 | ||||
ranking_weights = "race=1,qual=1" |
+0.0087±0.0047* | +0.0049±0.0034* | +0.0071±0.0023* | +0.0067±0.0020* | −0.0087±0.0036* (better every season) | win not better; head-to-head robust |
top_k = 3 |
−0.0062±0.0065 | −0.0006±0.0095 | −0.0078±0.0048* | −0.0047±0.0043* | +0.0046±0.0126 | not better |
top_k = 3 + team_effects |
−0.0084±0.0066* | −0.0013±0.0095 | −0.0107±0.0048* | −0.0066±0.0044* | +0.0004±0.0138 | win 2026-specific |
top_k = 3 + qual (the sweep's pick on 2024–25) |
−0.0051±0.0067 | −0.0030±0.0072 | −0.0107±0.0035* | −0.0064±0.0035* | +0.0052±0.0148 | not better on held-out 2026 |
top_k = 5 + qual |
−0.0050±0.0039* | −0.0043±0.0032* | −0.0065±0.0028* | −0.0053±0.0019* | +0.0033±0.0150 | win robust, podium robust, but not the pre-registered pick: proposed, needs its own decision-log row and a fresh confirmation |
team_effects alone |
+0.0147±0.0055* | +0.0073±0.0050* | +0.0077±0.0030* | +0.0094±0.0027* | +0.0044±0.0041* | not better |
The learns-nothing control is worse than everything (+0.0341±0.0071 on win). The top_k sweep's criterion was fixed before
reading (lowest pooled 2024–25 win log loss, qualifying on): it picked k = 3, which is not better on 2026, so k = 5's result
is read as a hypothesis, not a choice. Pre-registered for NASCAR and MotoGP* (not run: the local pull needs the owner's
sign-in): three runs each against global, the defaults, ranking_weights = "race=1,qual=1" with tie_on = "time_ms"
(the ties live in NASCAR qualifying: 122 of 271 rounds; scored on race_pole too), and top_k = 5 + qual; no sweep. The
downhill runs are confounded (no results-level baseline, different fields, and the record-key bug in the roadmap) and carry no
verdict. The staging-vs-branch check of the default global F1 run is byte-identical (3 CSVs, 16,713 fair values), so the
results core's loading refactor in this batch changed no default price on real data.
F1-7 (optional): In-race trading (reference model C2)¶
Status: out of scope · Start only if the owner decides to trade during races
- Needs a live source (
racinglines/sources/openf1/) and a lap-level simulator. Both are new capabilities; nothing existing changes. - Race-time pricing still goes through an as-of cutoff, with the same leakage tests.
- No new live order path by default. Replay first, and any live trading requires the owner's explicit approval.
F1-8: Live paper-trade validation (Polymarket)¶
Status: built and scheduled for Polymarket; waiting for a venue to list a race. Since 2026-09-29 also built for Kalshi (signal engine venue setting, PR #34; f1 reconcile, PR #37; f1 scorecard, PR #28; championship sleeve, PR #29); the first live weekend is round 16.
Polymarket has listed none since round 15 (28 Aug 2026). Since 2026-09-28 the phase runs on
every venue that lists (Kalshi via F1-9, pulled forward), judged by the pre-registered
validation plan; calendar on the Roadmap · Behavior
change: none to pricing or the backtest; recommendations and paper fills only
Built (2026-09-27): the signal engine (racinglines strategy signals f1, run by the
LaunchAgent bet.racinglines.signals every 5 minutes) prices each stage as its
session data arrives, with the backtest's own code, and records each profile's
recommendations and paper positions. Profile A (update taker,
gridq+pretrain+reset, min edge 0.10, 0.05 on head-to-head, no pre-weekend
stage) runs for the demo taker; profile C (conservative maker on gbm,
5-pt disagreement filter, 25-share quotes) for the demo maker.
scripts/signals_parity.py checks that a replay of the engine equals the sweep's
trades on a past weekend (2026 round 15: 11 of 11 taker trades and the maker's
15 fills identical). No order is placed (POLYMARKET_TRADING_ENABLED unset).
Details: Paper trading.
To do (items in Roadmap):
- Run A and C live on every weekend Polymarket lists, through 2026 and into 2027.
- After each weekend, compare live paper fills and markouts with the backtest's replay of the same weekend under the conservative "through" fill rule; log the markouts.
- Walk-forward re-run of A, A-lite, C, B (#06) and A′ (#08) with rounds 16–17 added.
- Bankroll-aware sizing and a deployed-capital cap per account.
With no Polymarket markets for round 16 (the Bahrain GP at Sepang, 2–4 Oct 2026), its first live F1 test is a mock private book on all of Polymarket's usual race markets, updated at session ends, as trialled on the Whistler downhill final: F1 live test.
Done when: 4–6 live weekends are logged and compared with the backtest, and the owner has decided on sizing (e.g. A-lite → A). Real orders need F1-9's CLOB V2 item and the owner's explicit approval.
F1-9: More exchanges (Kalshi, others)¶
Status: built (2026-09-29, PRs #26, #27, #34–#37, #41, #44): Kalshi connector run against the live read-only API (2026-09-28, markets/kalshi/); F1 history for 2025 and 2026 pulled and archived in data/archive/markets/kalshi/ (Data changes); the maker replay, the sweep and the taker signals all read it (venue = kalshi); tape-only NASCAR, MotoGP and IndyCar syncs (U9, fixture-tested); OG.com added as the first schema exchange (exchanges/og.toml, read-only). Open: the Kalshi recorder on the VM (U2), the first live run of the U9 tapes, and freezing profile K · Pulled forward (2026-09-28): the Kalshi recorder and Kalshi paper signals (Roadmap U1, U2, U5) come before F1-8's first live weekends, since Kalshi may be the only venue listing races this year
- Kalshi connector in
racinglines/markets/kalshi/, mirroringmarkets/polymarket/: markets and resolution rules, prices, trade tape, and order placement behind a flag (off by default). Registered inmarkets/venues, so the sweep, signals and the Markets page read it like Polymarket. - Store each market's resolution rules; never treat markets on two venues as linked when the rules differ.
- Polymarket CLOB V2: migrate
markets/polymarket/trade.pytopy-clob-client-v2before any real Polymarket order (see F1-0). - Other exchanges only if they list motorsport or cycling with real depth.
Done when: Kalshi's F1 markets replay in the sweep and appear in paper signals, with leakage-rule tests like Polymarket's.
Met 2026-09-29 (PRs #34, #41): Kalshi's F1 markets replay in the sweep and appear in paper signals. Exchange coverage by sport: Coverage.
Decision log¶
| Date | Phase | Decision | Why | Supersedes |
|---|---|---|---|---|
| 2026-10-22 | Core v2 / F1-6; C48 | Proposed; owner to decide. NASCAR sweeps only, not live or a default promotion. Candidate: arm (a), run 12028's exact recorded rank_logit settings from 11914, except ties off (tie_on=""); fitted temperature stays off. Retain ranking_weights=qual=1,race=1, covariates=grid,qual_gap, place_decay=2.0, half_life_days=240, sd_athlete=1.5, sd_team=0.75, team_effects=false, dnf=true; comparison protocol: seasons 2024–2026, after Q, seed 42, 4,000 sims, cache off, source SHA 7ca2206a |
Owner-supplied paired evidence, 92 races after Q: arm (a) minus calibrated global baseline 11939: win −0.0112 ± 0.0054, podium −0.0147 ± 0.0064, top 10 −0.0062 ± 0.0129. Fitted temperature (12029) worsened win by +0.0074 ± 0.0027 and podium by +0.0135 ± 0.0047 versus (a). Removing ties (12030 versus 12029) had no measured effect. Removing qualifying-as-ranking (12033 versus 12029) cost +0.0010 on win and +0.0041 on top 10. Track unresolved top-10 calibration error: 4.5 percentage points versus baseline 11939's 2.8. Raw/no-ties is a proposal, not an additional measured arm | No live/default decision; records the proposed NASCAR sweep candidate after the preregistered ablations |
| 2026-10-22 | Core v2 / F1-6 | Tighten existing opt-in P-L temperature=fit only with identity until 8 usable OOS races; one temperature per sport and stage, window 40 and bounds 0.5–3.0 unchanged. All arms retain the existing event-date history rule; settlement gating is deferred for every model together after timing backfill |
Owner's pre-registered ranking comparison: no unequal history gates or global calibration resampling for P-L. NASCAR: 11914 raw, temperature fit, fit minus ties, fit minus qualifying ranking (covariates and place decay unchanged). MotoGP: global defaults, 11976 settings over all 59 races, global noise 0.6. Seven runs only; disk caches off and cache=off in each label; committed SHA, push approval first, no merges/deploys before Sunday's races |
Proposed settlement gate for fitted-temperature pricing only; unchecked assumption that calib_power=walk_forward calibrated both models and both sports |
| 2026-09-26 | — | Roadmap aligned to the existing repo; position_sim is the baseline and stays the default |
Avoid parallel pipelines; keep the golden suite meaningful | — |
| 2026-09-26 | 0 | Reference runs: backtest 115 (129 races, three stages), sweep 191 (2026 rounds 1–15, practice prior), season strategy 113 | The sweep was re-priced with the practice prior after the roadmap was drafted; run 96 is superseded | — |
| 2026-09-26 | — | Phases renamed F1-0 … F1-7 | Avoid confusion with the platform phases in the project history | — |
| 2026-09-26 | — | Platform phase 3 (sport schemas) is additive only, scheduled between F1-0 and F1-1 | Owner's decision: keep schema-driven sport differences without breaking the "nothing moves" rule | — |
| 2026-09-26 | 1 | Add a f1 compare command; the Lab has no paired comparison to reuse |
The promotion rule needs repeatable ± 2 SE tables | — |
| 2026-09-26 | 4 | Add the stage-aware taker to F1-4 | Sweep run 191's stage split is the clearest measured taker gap | — |
| 2026-09-26 | 0 | Order signing is V1 and broken on production. Fix separately: migrate trade.py to py-clob-client-v2, with dry-run tests, before anyone enables trading |
Polymarket changelog: no V1 compatibility since 2026-04-28. Kept out of F1-0 (read-only check) | — |
| 2026-09-26 | 0 | Market recorder runs as a LaunchAgent (KeepAlive, 60 s restart throttle), logging to data/runs/logs/record.log |
A plain background process doesn't survive a reboot; F1-4 needs continuous book depth | — |
| 2026-09-26 | 0 | F1-0 done | Docs build, suite green (118), reference runs logged | — |
| 2026-09-27 | platform | Sport schemas are TOML (sports/*.toml), read with the standard library; the registry, both models, the sweep and the venue matrix take their constants from them |
No new dependency (no YAML library is installed); constants proved identical before and after, goldens unchanged | the YAML plan |
| 2026-09-27 | 1 | Variant switches live in model.py (defaults = baseline) and are named in variants.py; --variant on the f1 command; stored sweep stages, season forecasts and runs record their variant; the web app reads baseline runs only |
One pricing path; a variant's cached pricing is never mixed with another's | — |
| 2026-09-27 | 2 | GBM uses scikit-learn's HistGradientBoostingRegressor instead of LightGBM | Already a dependency (no optional extra needed); supports monotone constraints and sample weights | the LightGBM extra |
| 2026-09-27 | 2 | gridq added after seeing that grid hurt when the grid is simulated |
Restricting the term to known grids is the principled reading; flagged as a choice made on this data | — |
| 2026-09-27 | 2 | Recommend promoting gridq + pretrain (run 423 vs 193: better at every stage it touches, worse nowhere) |
Meets the backtest half of the promotion rule; promotion changes live prices, so it waits for the owner | — |
| 2026-09-27 | 2 | gbm not promoted (run 665 vs 193: worse on teammate h2h at every stage, on podium and win log loss before and after qualifying; better nowhere) |
Fixed settings, no tuning; the ridge with gridq already captures the front-of-grid curve |
— |
| 2026-09-27 | 3 | tail not promoted |
Neutral at every stage (no metric beyond 2 SE) | — |
| 2026-09-27 | 3 | Prop markets (safety car, red flag, rain) deferred | No strategy trades them yet, and calibrating them needs the prop markets' history mapped in the sync classifier; the per-circuit rates now exist (race_disruption) |
— |
| 2026-09-27 | 4 | Maker options chosen a priori (flatten fully before qualifying at mid ± 1¢; info skew ×(1 + 2·e^(−t/2h)); 1.5× half-spread on kinds with negative cumulative 60-min markouts in earlier weekends) | No tuning on the 2026 sweep | — |
| 2026-09-27 | 4 | Queue model, liquidity rewards and Kelly caps deferred | No historical book depth or reward data for the 2026 weekends; Kelly caps are optional | — |
| 2026-09-27 | 5 | F1-5 deferred | See the phase: coherent by construction; hedging needs recorded depth | — |
| 2026-09-27 | 2 | Owner: no promotion or default change for now; keep iterating on the variants and combos | Owner's decision | — |
| 2026-09-27 | — | Championship checkpoints (f1 season-checkpoints): enter pre-season, after 3 and after 6 GPs (owner's choice), hold, score each over the next 3 GPs and to date with P&L, drift toward our fair value and the edge-closing slope |
The update-every-race strategy mostly measures reaction speed; fixed entries and equal windows compare models without picking the windows after the fact | — |
| 2026-09-27 | 2 | reset variant: in a new-regulations season (sports/f1.toml [regulations] resets), earlier seasons' car pace weighted ×0.25 |
The 2026 checkpoints showed the car-pace layer carrying 2025 over (McLaren 84% pre-season). The weight is set a priori, and it's judged on 2022 too, since the idea came from 2026 data | — |
| 2026-09-27 | — | users.prefs (nullable JSONB, migration 5b1e0c7d2a41) for the Lab's Edge Finder combos and last-used job knobs |
Owner: settings follow the account; the database is the source of truth for history and setup, the browser only for view state. Additive column | — |
| 2026-09-27 | — | Owner: the repo is private; a minimal data set is committed (FastF1 raw files, the Polymarket prices/trades archive, cloud search results), allow-listed in .gitignore and tests/test_no_data_in_git.py |
Cloud sweep sessions clone the repo and can't be copied into; the owner preferred this over a storage bucket. Remove from history before any public release | the "zero data in git" rule |
| 2026-09-27 | — | 2025 Polymarket markets synced (pm-sync --closed): 2025 title formats ("F1 X Grand Prix Winner", "– Pole Winner", "Which Constructor scores the most points?", "Head to Head Matchups") and Grand Prix name aliases (Brazilian → São Paulo, …) added to the classifier, after the 2026 forms |
2025 is the held-out season for everything built on 2026. Every stored 2026 title classifies exactly as before (checked) | — |
| 2026-09-27 | — | Cloud sweeps: racinglines strategy search f1 runs a queue of sweeps in parallel and re-reads the queue so an agent can steer it; protocol in Cloud sweeps, with a held-out-season rule for anything tuned |
Long unattended searches overfit 15-24 weekends unless tuning and confirmation use different seasons | — |
| 2026-09-27 | — | One settings schema (racinglines/pipelines/sweep_settings.py) drives the sweep flags, the Lab's Edge Finder sweep form, search queues and saved params; stage prices are cached by model settings + the data each stage saw |
Owner: any combo found must be recreatable in the Lab; defaults reproduce the old sweep exactly (checked, zero differences) | the four taker knobs |
| 2026-09-27 | — | Searches: 2026 (every raced weekend) is the primary window; 2025 is "live from its first race", a secondary, stable-rules window; each season's default baseline runs in every search on the same data; the cloud agent may add new switches in code under the ground rules | Owner's choices; results labelled robust / 2026-specific / not better | — |
| 2026-09-27 | — | All HTTP data sources paced per host and retried with backoff (racinglines/sources/http.py); FastF1 keeps its own limiter |
Owner: respect API limits; two Polymarket timeouts during the 2025 download | ad-hoc retries |
| 2026-09-27 | 4 | min_edge_h2h: a separate minimum edge for head-to-head markets (empty = same as min_edge); unset optional settings are left out of the settings hash |
With it, A makes +$1,243 in 2026 and +$1,363 in 2025 (+$1,232 / +$1,237 without). Leaving unset optionals out keeps every earlier run's key, so cached stages and saved runs still match | — |
| 2026-09-27 | 8 | Paper-trading defaults: profile A (update taker, gridq+pretrain+reset, min edge 0.10 / 0.05 h2h, no pre-weekend stage) for the demo taker, profile C (conservative maker, gbm, max disagreement 0.05, 25-share quotes) for the demo maker; live from round 16 of 2026 |
The only two params-4h candidates profitable in both 2025 and 2026 with small drawdowns. Recommendations and paper fills only; the signal engine reuses the backtest's code, checked by scripts/signals_parity.py |
— |
| 2026-09-27 | 8 | Demo accounts' history is evidence-driven: weekends before live paper trading are backtest replays (flagged detail.backfill), and the maker's strategy switches follow a fixed rule on walk-forward Edge Finder evidence (pipelines/story.py) |
A demo track record must be reproducible and must not use hindsight; labelled as a replay in the app | — |
| 2026-09-27 | — | Web routes renamed to match their pages (Markets, Strategy, Positions, My Book, Lab); every old URL redirects (web/legacy.py) |
A page's name and its URL should agree; redirects keep bookmarks and links working | the old routes |
| 2026-09-27 | — | Cloud runs load a database snapshot (racinglines db snapshot-export / snapshot-import, data/archive/db/) plus exported market links, prepared by scripts/cloud/prepare.sh |
An exact replica (same ids, same prices) makes cloud results identical to local ones and needs no network beyond PyPI and GitHub | f1 ingest + pm-sync on the VM |
| 2026-09-27 | 9 | New phases F1-8 (live paper-trade validation) and F1-9 (more exchanges); the Kalshi package moves from F1-5 to F1-9 | Trading on any exchange only after paper-trade validation; Kalshi lists F1 | the Kalshi row under F1-5 |
| 2026-09-28 | 8 | Order signing moves to CLOB V2 (py-clob-client-v2==1.2.0); V1 client removed. Dry runs sign with the local order builder (V2 struct, domain version POLYMARKET_ORDER_VERSION, default 2), no network; live orders ask the CLOB for its version |
V1 orders stopped working on 2026-04-28, so there is no working behaviour to keep behind a switch. The client’s create_order calls the CLOB for the tick size and version even in a dry run. Not verified against the live CLOB: no order sent |
the V1 client |
| 2026-09-28 | 8 | Bankroll-aware sizing and a deployed-capital cap are optional sweep settings (bankroll, max_deployed, unset = today’s fixed sizing). The taker is now a per-market stepper (taker_weekend._Market); with a cap, markets are stepped in time order and buys over the cap are cut to fit |
Default results identical to the old taker on 2,400 random synthetic weekend × parameter combinations, and the 2026 sweep reproduces +$1,874.01 for the stage-aware taker. Choosing a rule waits for live weekends | — |
| 2026-09-28 | 2 | A Monte Carlo seed setting (seed, a model setting, unset = today's fixed 42) for the sweep, the search and the signal engine's profiles |
A search's noise replicates re-ran the same draw; independent seeds show whether a winner is noise. Unset leaves every model key, cached stage run and price unchanged (pinned in the tests) | — |
| 2026-09-28 | 3 | Props priced outside the simulator (models/props.py): safety car, red flag and rain as per-circuit rates shrunk to the field rate (prior 16 races), fastest lap from the stage run's finishing odds × how often each finishing bucket set it; listed in a live book only when its [live.markets] kinds names them |
Walk-forward on 2022–2026 (107 races, f1 props --check), per-circuit rates don't beat the field rate within the noise (Brier: safety car 0.2535 vs 0.2525, red flag 0.1128 vs 0.1113, rain 0.1754 vs 0.1765, SE ≈ 0.013–0.02), and lighter shrinkage (4) was worse; the prior was picked on the same data. Fastest lap isn't calibrated yet: it needs stored stage runs for past races (the bucket's dump). The sync classifier still leaves Polymarket's prop markets unmodeled |
the simulator-based props |
| 2026-09-28 | 9 | Kalshi connector (markets/kalshi/) writes the same tables as Polymarket (market_links with exchange='kalshi', one row per market's YES contract; market_trades, market_price_history, market_book_snapshots), keeps each market's rules in params.rules, and gates orders like Polymarket (post-only, a notional cap, KALSHI_TRADING_ENABLED). Built early at the owner's request |
Owner asked for it (2026-09-28). The cloud network blocks Kalshi and there are no credentials, so it's tested on mocked responses shaped like the API docs; the title wording, series tickers and field names need checking against the live API before anything reads it | — |
| 2026-09-28 | 4 | The maker replay's positions table keeps its columns when a weekend has no markets of the chosen kinds, so it summarises to a zero total | The fix for the h2h-only crash on 2025 (no h2h markets before round 8). Weekends with markets are unchanged; rounds 8–9 h2h-only run end to end on the bucket's database | — |
| 2026-09-28 | 4 | The stage-aware taker stays a flagged, in-sample reference strategy; no profile adopts it | Out of sample on 2025 it lost $316 over 23 weekends (+$1,874 in-sample on 2026); 2025's best update-taker stage was after qualifying, the stage it skips | — |
| 2026-09-28 | — | Proposed Google Cloud shape: Cloud SQL + Cloud Run (one image, a service for the web app, jobs for the recorder, signal engine, live events and sweeps) + Scheduler, the bucket mounted as the data root, IAM database login, no service-account keys | Scales to zero between race weekends; every piece is already a CLI subcommand; RACINGLINES_DATA already moves the data root. A proposal: nothing built |
a VM running today's LaunchAgents as cron jobs |
| 2026-09-28 | 2 | New variant rookie (model.ROOKIE_CARRY, off by default): a finished rookie season's teammate comparisons count 0.25 in the driver offsets, for both drivers of the team-event; drivers already in the data's first season aren't treated as rookies |
The weight is set a priori, like reset's. Judged on all 129 races and per season (runs 1288, 1289): win odds improve before qualifying, teammate h2h better in 2026 and worse in 2021, so not promoted |
faster forgetting for every driver (a shorter driver half-life), not tried |
| 2026-09-28 | — | Downhill points tables per era live in points_schemes, entered from a TOML file (sports/points/uci_dhi_wc.toml); the model reads them only with --points db, by setting the three table globals per season (points.use) |
No plumbing through every function, and the default path is untouched. Placeholders stay marked (official = false) until the owner enters the UCI scales; committed standings are TOML because git ignores CSVs |
a points argument threaded through the season and weekend functions |
| 2026-09-28 | — | Downhill UCI IDs: a separate uci_id field and a uci athlete identifier matched first; the parser's rider_id stays name-based; existing duplicates are merged by an explicit command, never automatically |
Old files and today's model inputs stay identical; a merge changes history, so the owner runs it | switching rider_id to uci:<id> in the parser |
| 2026-09-28 | 8, 9 | Strategy to the end of 2026 (Strategy 2026): real markets first on any venue (T1), tapes of unmodeled sports recorded (T2), private books only where no venue lists (T3). Kalshi work pulled ahead of F1-8's first weekends; A and C frozen to 31 Dec; a Kalshi-tuned maker K chosen by its own sweep; validation rules pre-registered in Paper trading | Polymarket has listed no race since 28 Aug; C loses on Kalshi's 2026 tape (−$119) while it earns on Polymarket's (+$653); the taker needs ~40 live weekends to prove, the maker ~17 | F1-9 "start after F1-8's first live weekends" |
| 2026-09-29 | 9 | Kalshi maker profile K chosen by its own sweep on Kalshi's tape: gbm, 2¢ quotes, max_disagree 0.10, 25 shares, maker_min_volume_24h 400. 2026 +$490, 2025 +$659 (16k simulations: +$453 / +$741). Freezing is the owner's call |
Held-out rule (2026 primary, 2025 confirm) and the 16,000-simulation confirmation; beats profile C by +$609 (2026) and +$150 (2025, within the noise). The first 2025 figure, +$855, missed Imola 2025 (Kalshi-only listing, −$195): sweeps now read the venue from the profile | the first K table, +$855 |
| 2026-09-29 | — | Sweeps run in grid mode (up to N sweeps of one season and model per process), the rating history is built lazily and can be cached, and parallel defaults to every core; Kalshi's maker and taker fees are defined once on the venue classes |
The 106-job Kalshi search fell from 59 to 21 minutes with identical saved runs (checked to the cent) | one process per job |
| 2026-09-29 | 9 | Exchanges are schemas, not code: exchanges/<code>.toml read by one generic driver (markets/exchange_driver.py). OG.com is the first, read-only, off by default (RACINGLINES_OG_VENUE); its fair-price indicator is an indicator only, with no backtest (about a month of history, asks-only books at 1–4¢). Polymarket and Kalshi keep their own packages for now |
Owner's standing principle (2026-09-29): a new venue should be a config file, not new code paths. The $0.02 fee is from reviews and is unconfirmed | — |
| 2026-09-29 | — | CLAUDE.md is the standing process note for Claude Code sessions: environments, model tiers, dev cycle (one thread per task), pre-merge checks, report format, safety rails | Owner asked for one place for how the sessions work; the domain lives here and in the Roadmap | — |
| 2026-09-30 | 8 | Three opt-in sweep settings from the Monaco 2026 Hadjar check (all unset by default, so every result and key is unchanged): thin_edge_mult (a taker trades a market under the volume floor when the edge is at least that many times the minimum and a recorded book, at most 10 min old, shows size at the touch; buys only, stake capped at that size), market_kinds accepts race_top10 (Kalshi's top-10 finishers, a group of ten for the coherence check; not in the default set, taker only since the maker replay never modelled it), coherence_tol_by_kind (per-kind group tolerance over 0.25, e.g. race_podium=0.35). No default changes; a tuned value needs a search on 2025 and 2026 and its own entry here |
Hadjar's Monaco podium markets (Polymarket 3.5c with $1-10 traded in the prior 24 h, Kalshi 2-7c with $15-44) failed the $50 volume floor at every stage, and the podium group sum failed the 25% tolerance at three stage/venue pairs; the floor cannot see whether a book had size. Owner asked for all three to be built (2026-09-30). The live call on the Markets page still applies the plain volume floor |
— |
| 2026-09-30 | 9 | NASCAR / MotoGP taker replay (racinglines strategy replay nascar, motogp replay; pipelines/position_replay.py), off by default, a separate path from the F1 sweep. A priori settings in sports/<code>.toml [replay], none tuned: stages T-3d / T-1d / race eve (hours from 00:00 UTC on race day), the F1 taker's defaults (min edge 0.05, $250 per edge, $50 cap, 1¢ cost), the F1 volume floor ($50 / 24 h) and coherence tolerance (0.25) on race-win and top-3 groups only, model seed 7, the schema's pricing_model with its default settings. Kalshi's taker fee is reported and netted |
No evidence yet on these sports, so nothing is chosen from their data; any change after the first replay needs its own entry and a held-out season, as the F1 rule. Top-5/10/20 listings are often partial, so their sums are not checked | — |
| 2026-09-30 | 9 | Owner: the new sports and exchanges are on by default. NASCAR's board columns are win / top 3 / top 10 and MotoGP's is win (standard_kinds); MotoGP's matcher is its [identity] resolver, so the Kalshi, Polymarket and OG.com syncs now fill rider, race and kind on MotoGP links (prediction stays unmodeled); nascar replay / motogp replay run on every exchange the sport lists by default (--venue all); their --save writes prediction records and sims unless RACINGLINES_PREDICTION_RECORDS=0 |
Owner's instruction, 2026-09-30 03:07Z. Still off: every F1 default and output (unchanged), the #81 switches (thin_edge_mult, top-10 as a traded kind, coherence_tol_by_kind), any tuned taker setting, the trading flags; --save still needs a fresh backup |
the replay-only [replay] note of the entry above |
| 2026-09-30 | 9 | Bug fix, owner's call: the NASCAR and MotoGP recent-form weights (recency_decay) now run from e^-decay on the oldest race in the window to 1 on the latest; they ran the other way, so a driver's oldest result counted most. Changes every NASCAR/MotoGP walk-forward, search and replay price (no goldens pin them); F1 and downhill untouched |
The owner: backtests of these sports are meaningless with the weights reversed. recency_decay keeps its default (2.5), still untuned; any earlier NASCAR/MotoGP search ranking is void |
— |
| 2026-09-30 | 9 | Two more NASCAR / MotoGP model fixes before the backfill: (1) MotoGPRace.load reads the race round only (it joined every session the ingest stores, so practice, qualifying and sprint classifications counted as race form); (2) history_races keeps the last N races and team form averages the team's last recent_races races (each race's mean over its entries, recency-weighted), where both counted result rows (a two-car team's 6 "races" were 3) |
Owner: the backtest must be right before the overnight backfill. Changes NASCAR/MotoGP prices only; recent_races and history_races keep their defaults, untuned |
— |
| 2026-09-30 | 9 | The weekend taker replay pays the venue's taker fee (TakerParams.taker_fee, from venue_replay.EXCHANGES: Kalshi 0.07 × contracts × P × (1 − P) per order, rounded up to the cent; Polymarket 0) |
Kalshi taker sweeps charged only the flat 1¢ cost. Polymarket's rate is 0, so every Polymarket result is unchanged (tested) | — |
| 2026-09-30 | 9 | Kalshi taker results are left out of every ranking until the replay fills at a tradeable price | Kalshi's stored price is each candle's last trade, and thin books print at extremes: 1.67% of Kalshi F1 points are 15+ points off their rolling median (5.6% on head-to-heads) against 0.06% on Polymarket. The taker buys at those prints (taker-resweep report, section 3). Needs a bid/ask re-pull from a machine with Kalshi access | — |
| 2026-09-30 | 8 | Taker re-sweep (sweeps/taker-resweep.toml, 212 sweeps, 16k confirmations): A stays the core taker; the Pro shortlist is T1-T10 (A and its nearest variants, and the stage-aware taker on A's settings), the recommended blend TB = A's update + stage-aware takers at half stake each (16k: 2026 +2,048, 2025 +989); Basic draws 3 of the 10 per account from a fixed seed. No default changes; PR #81's switches stay off (thin_edge_mult inert on history, podium coherence 0.35 within noise) |
Held-out rule with a measured noise floor (update ±400, others ±700). Nothing beats A beyond noise in both seasons; volume floor 0 excluded (untradeable last prices); the blend was chosen on both seasons, so it has no held-out season | profile A as Basic's only strategy |
| 2026-09-30 | 9 | NASCAR Cup season forecast (racinglines model season nascar, models/nascar_season.py), read-only and off by default (RACINGLINES_NASCAR_SEASON=1): the rest of the season from the points standings through the 2026 Chase (top 16 on points, reset 2100/2075/2065/2060/2055 then −5 to 2000, 10 races, no eliminations; 55 for a win), each race priced by the schema's pricing_model. A priori settings, none tuned: stage finishes = the race order re-drawn with N(0, 5 places), the one bonus point a race to the best of the order re-drawn with N(0, 6), 4,000 sims, seed 7, the race model's defaults except noise, which is measured (the SD of each finish around the driver's previous-6-race mean over the last two seasons) because the default 2.0 gave the points leader 94.6% on the Mac spot check against 40-55% on all three exchanges; the replay keeps 2.0 (an owner decision to change). Today's standings come from NASCAR's points feed when on disk (penalties, official seeds) |
No season-market history to tune on yet; races are simulated independently (no form drift, no track types), which understates the spread, so the prices are an indicator, not a trading input. The bonus point's rule is inferred from the 2026 feeds. Any tuning needs its own entry | — |
| 2026-09-30 | 9 | Overnight VM run (106-job broad sweep at 4k, top 10 at 16k; report reports/2026-09-30-overnight-vm-run/report.md): profile A stays the core taker; no default changes. NASCAR × Kalshi taker grid: defaults stay (min edge 0.05, $50 floor), nothing tuned |
Held-out rule, noise floor ±352: no setting beats A by more than that in both seasons (best 2026 gains reset_weight=0.5 +400, gbm half-life 90 +405, both within noise or negative in 2025; best 2025 gain +222). NASCAR is +4.2k to +5.5k in 2026 (19-21 of 32 races up) but loses in every setting in 2025 (−242 to −390, 1-2 of 8 races up) on Kalshi last-trade prices: 2026-specific, not robust |
— |
| 2026-10-05 | 3 | Kalshi takers fill at the touch, by default. On Kalshi a taker buys YES at the ask and sells at the bid (NO: buys at 1 − bid, sells at 1 − ask), plus TakerParams.cost (slippage beyond the touch) and Kalshi's taker fee; the edge each side needs is fair − ask (YES) or bid − fair (NO). The quote is the recorded order book when a snapshot is at most 10 min old (venue_replay.Kalshi.BOOK_AGE; books every 5 min on the VM since 2026-09-30), else the last hourly candle's closing bid and ask (market_price_history.bid/ask, backfilled 2025-01-01 to 2026-10-05), else the old rule (last trade ± cost). Applies to the sweep, the Lab and live signals on Kalshi; Polymarket and OG.com stages carry no quote and are unchanged |
Owner's call (2026-10-05). The candle's last trade is not a price a taker could have had: 4.5 % of backfilled F1 hours have it outside the closing quote (median 1¢, up to 90¢ near settlement), and the old rule ignored the spread. The Kalshi taker results in Kalshi history were computed under the old rule and are re-run on the VM after this change (f1 sweep --venue kalshi) |
Kalshi taker fills at the candle price ± cost |
| 2026-10-05 | 8 | Rolling Kelly sizing is an optional sweep setting (kelly, a fraction; unset = today's linear sizing, so every result and settings key is unchanged). With bankroll set, a taker's stake is kelly × (q − c) / (1 − c) × balance at the touch, capped by max_stake × the bankroll scale and max_deployed. The search report adds P&L without the best two events and a shape label (steady / long-shot / loses; renamed steady / concentrated / mixed / losing on 2026-10-06, owner), and can rank at 16k (rank_sims). Queue sweeps/strategy-review.toml: every profile on Polymarket and Kalshi, half- and quarter-Kelly on $10,000 ($500 a market, $2,500 a weekend), and the every-kind variant. |
The owner's strategy review (2026-10-05): profile A on Kalshi rested on two weekends a season, so every strategy is re-checked for long-shot vs steady before the next race weekend. Kelly is a sizing option for the review; no profile uses it until the results are in. | |
| 2026-10-06 | 8 | Strategy review (Strategy review, 80 jobs at 16k on Polymarket and Kalshi, 2026 and 2025): A (T1) stays the core taker; full size on Polymarket, small on Kalshi until live weekends show its Kalshi edge isn't a few long shots; Kelly stays a review option, with quarter-Kelly preferred over half-Kelly if sizing ever moves to a bankroll. No default changes. | Polymarket T1 is steady (+1,375 / +1,432, still +112 / +350 without each season's best two weekends). Every Kalshi taker is concentrated or mixed: up in 2026 on Australia, negative in 2025 without its best two. Kelly totals come mostly from two compounding weekends and assume unlimited depth at the touch; QK T1 beats HK T1 in both seasons without the best two on Polymarket. | |
| 2026-10-04 | 3 | Provisional. New variant fastlap (model.FASTEST_LAP, off by default): the race simulation draws the fastest lap, the quickest classified car on race pace rp (with the simulation's pace shock) plus noise FL_SIGMA = 0.006 of a lap, FL_RHO = 0.3 of it shared by teammates; stored as race_predictions.extra.fl_prob, kind race_fastest_lap in markets/kinds.py, read by db.reads.model_prob, so Kalshi's KXF1FASTLAP links get a fair value from runs priced with the variant. Drawn on a side stream seeded from the main one without advancing it, so every other price is byte-identical with the switch on or off. No live config or profile turns it on |
The owner wants the fastest lap from the core engine, not the history-based props.fl_probs. Both noise values are set a priori, not fitted: the race-pace surprise (≥ 0.3 %, TEAM_DRIFT_SD; teammate correlation +0.63) and a best-lap-vs-median term of the same size, in quadrature. A retired car never sets it, and no late fresh-tyre effect (no data for one here). Uncalibrated: score extra.fl_prob against Kalshi's resolved markets (market_links.resolved_yes, prediction race_fastest_lap) on the VM before the owner turns it on, with its own Decision Log entry |
props.fl_probs as the fastest-lap price for these links (declined; the live props book keeps it) |
| 2026-10-04 | 3 | Kalshi's top 5 (KXF1TOP5) and biggest mover (KXF1BIGGESTMOVER) are priced from the race simulation: kinds race_top5 (classified in the top 5) and race_biggest_mover in markets/kinds.py, stored by position_sim as extra.top5_prob / extra.mover_prob from the draws it already makes (no new randomness, every other output unchanged). Biggest mover: among the cars that finish, the largest gain from the simulated grid to the finish; nobody wins if no one gained; every driver tied on that gain counts as YES. Both kinds are left out of the default record and walk-forward sets (Kind.default = False) |
The owner asked to link every market we can. Kalshi's rule says "the largest positive differential between their starting grid position and their finishing position" and doesn't say how a tie resolves; YES for each tied driver is the literal reading. The simulated grid is the qualifying order, so grid penalties and pit-lane starts aren't modelled, and a retired car is never the mover. Unchecked: neither price is scored against Kalshi's resolved markets yet, and our settlement returns no result for the mover (the results store no starting grid) | Leaving both unmodeled |
| 2026-10-06 | 3 | Provisional. Red flag and safety car given wet or dry (models/props.py): a race is wet when any weather sample had rain (rain_share > 0, the race_rain rule); rate_given is the circuit's rate among its wet (dry) races shrunk to the field's wet (dry) rate, itself shrunk to the overall field rate (both with PRIOR_N 16); rate_wx = p_wet × wet rate + (1 − p_wet) × dry rate, with the circuit's shrunk wet share when no p_wet is given. f1 props --check adds climatology and wet_oracle (p_wet = the realised wet flag), and --history <csv> runs it without a database. Nothing is on by default: the live book keeps rate(); market_set(..., p_wet=) prices race_red_flag (WX_KINDS) with rate_wx only when a probability of rain is passed, which a weather forecast (racinglines/weather) will supply; the safety car is left on rate() after the numbers below |
2020–2026 history (147 races): red flags in 10 of 35 wet races, 12 of 112 dry. Walk-forward 2022–2026, 108 races, PRIOR_N 16, Brier (se): red flag circuit 0.1119 (0.0201), field 0.1105 (0.0203), climatology 0.1109 (0.0209), wet oracle 0.1057 (0.0198), log loss 0.3872 → 0.3666; safety car circuit 0.2529, field 0.2518, climatology 0.2516, wet oracle 0.2575 (worse). Paired against circuit: red flag wet oracle −0.0062 (se 0.0045), climatology −0.0011 (0.0012); safety car wet oracle +0.0046 (0.0027). PRIOR_N 8 is worse for every shrunk method (the field rate doesn't use it). So a good rain forecast is worth something for red flags only (about 1.4 se, an upper bound), climatology adds nothing, and the safety car should stay on rate(). Lighter field-level shrinkage (2 races, tried ad hoc) changed nothing |
— |
| 2026-10-06 | 3 | Provisional. Retirement kinds in markets/kinds.py, all default=False (summary, records and walk-forward unchanged): race_retire (1 − classified; settles True on DNF or DSQ, void on DNS or a driver absent from the results), race_n_retirements (DNF + DSQ over a line), race_first_retirement and race_first_retirement_team under a uniform timing assumption: the simulation draws who retires, not when, so every retiring car in a simulation is equally likely to be first out (P = mean of dnf / n_dnf, 0 when nobody retires); settled on a laps_completed column of the results frame (fewest laps among DNFs, ties all YES; None without it; NO for everyone when nobody retires). Owner (same day): race_second_retirement and race_third_retirement carry the same timing assumption (P = the first-retirement share in simulations with at least n retirements; settled by competition ranking on laps completed, so a tied place leaves the next one empty), and the team versions race_team_any_classified and race_team_both_points (n = 10) join race_team_both_classified. DNF calibration check: f1 props --dnf-check <csv> (models/dnf_check.py) |
Position simulation dnf_prob, live default variant gridq+pretrain+reset, latest as-of run per race with the qualifying order known, 829 rows, 40 races (2025-01 to 2026-16); 815 scored (10 DNS, 4 without a result left out). Mean predicted DNF 12.4 %, realised 13.5 % (2.52 vs 2.75 per race). Brier: model 0.1140 (se 0.0088), field rate (in-sample) 0.1168, race mean 0.1167; paired, the model beats both by 0.0028 (se 0.0017 / 0.0015). By bucket (n, mean p, realised): 0–5 % 31, 4.1 %, 3.2 %; 5–10 % 245, 8.3 %, 13.1 %; 10–15 % 384, 12.2 %, 9.9 %; 15–20 % 97, 17.0 %, 17.5 %; 20 %+ 58, 27.4 %, 37.9 %: slightly under-predicted overall and too compressed at the top. First retirement has no timing to test; it waits for laps completed in the results |
— |
| 2026-10-06 | 3 | Provisional. Wet-race forecast into the props (racinglines/weather/wet.py, Weather: wet-race forecast): seven Open-Meteo global models each vote wet when they forecast ≥ 0.1 mm in any hour from start − 1 h to start + 3 h; p_wet = (votes + 3 × climatology) / (models + 3), climatology the circuit's shrunk wet share. All four settings (models, 0.1 mm, the window, prior 3 votes) were fixed before the backtest, not tuned on it. On when a vote is saved: racinglines weather fetch ... --session race=... writes it, and the live book prices race_rain at p_wet and race_red_flag with rate_wx(p_wet); the safety car stays on rate(); DNFs and the position simulation are unchanged; [live.props] weather = false turns it off. No vote, no change. Naming (owner): a weather-aware method, variant or strategy is its base name + -WX |
racinglines weather backtest --lead 5, walk-forward, 63 races 2024-01 to 2026-15 (13 wet, 6 red), the vote issued 5 days before each race (Previous Runs API; fixture tests/fixtures/weather/open_meteo-leads-f1.csv). Rain Brier: circuit history 0.1668, circuit-WX 0.1232 (paired −0.044, se 0.021; log loss 0.516 → 0.388); by lead 1–7 days 0.093, 0.104, 0.113, 0.113, 0.123, 0.115, 0.135. Red flag: climatology 0.0915, climatology-WX 0.0917, wet oracle 0.0900 (neutral; 2 of 6 red flags were wet). Safety car: climatology 0.2667, climatology-WX 0.2679 (no effect, stays off). DNFs per race: wet 2.71 vs dry 2.42 (2020–2026, 34 wet of 146), but the forecast-mixed count doesn't beat the field mean (MAE 1.411 vs 1.416, Poisson deviance 1.378 vs 1.373), so no DNF change. Prior 1 / 3 / 7 votes: rain Brier 0.125 / 0.123 / 0.128 (information only) |
— |
| 2026-10-06 | 3 | Provisional. Sprint stage = the GP finishing model with the schema's sprint points and the SQ grid (sports/f1.toml [sessions.sim]: sprint = {grid_from = "sprint_qual", grid_from_by_year = {2021 = "qual"}, points = "sprint", dnf_scale = 0.5}; models/inputs/laps_pricing.price_stages). The grid is SQ's laps ranked by best valid lap once SQ has run (FastF1 stores SQ without positions), else simulated from qualifying pace; after the sprint, its actual result. grid_from_by_year: in 2021 Friday's qualifying set the sprint grid (no Sprint Qualifying), so the 2021 sprints take their grid from qual (model.grid_source). dnf_scale 0.5: each driver's GP DNF rate × 0.5 in the sprint, until it is refit after the 2023–25 sprint backfill (12 more weekends). No sprint grid weight or noise setting. Drawn on a side stream, so every Grand Prix price is byte-identical. Writes extra.sprint_win_prob / sprint_pole_prob, which db.reads.model_prob already reads before the race-odds stand-in. Validated on 13 sprint weekends (racinglines model sprint-check f1, owner's local database, 2026-10-06, at this commit). With the grid session's order known, sprint-winner Brier 0.0322 (stage) vs 0.0369 (GP-win stand-in), paired diff −0.0047 (se 0.0009), log-loss diff −0.0194 (se 0.0032), the stage ahead on 12 of 13 weekends (Monza 2021 +0.0003). Before the grid session the stage is a coin flip against the stand-in (diff +0.0002, se 0.0003). Observed DNF share 7.1 % (sprint) vs 14.8 % (GP), ratio 0.48; simulated 5.9 % vs 11.8 %, so the model under-predicts retirements at both. Before the per-year grid source the two 2021 weekends lost to the stand-in (grid simulated: Brier 0.0342 vs 0.0328 and 0.0389 vs 0.0327); from qual they are 0.0323 vs 0.0328 and 0.0327 vs 0.0324 |
dnf_scale: on the 13 stored sprints vs the GPs of the same weekends (owner's local database, 2026-10-06), the DNF share is 7.0 % vs 14.4 %. Context, not settings (same 13 weekends): grid-to-finish Spearman 0.865 (sprint) vs 0.770 (GP), and pole converts to a win 69 % vs 46 %. So a sprint follows its grid more closely than a GP; a sprint-specific grid weight waits for the backfill and its own row. The shape (a second simulate_race with sprint points) is what the season forecast already does for sprint points |
Sprint winner and sprint pole priced from the race's win and pole odds (U5 stand-in, kept as the fallback for runs without the stage) |
| 2026-10-06 | 3 | Provisional, not promoted. model-WX, the retirement twin of the weather-aware props (models/dnf_check.py, F1: weather-aware (-WX) variants): each driver's dnf_prob times props.wx_scale (the circuit's retirement rate at the forecast's p_wet over the same at its usual wet share), the forecast being weather/wet.p_wet_series (the 5-day Open-Meteo vote shrunk to the circuit's wet share, as the wet-race row above). It is a check only: no default changed, the live book's retirement and classification kinds keep the run's dnf_prob, and gridq+pretrain+reset-WX stays unbuilt (todo P2). model-WX is not better than model |
racinglines model props f1 --dnf-check <dnf_asof.csv> --history <history.csv> --forecast tests/fixtures/weather/open_meteo-leads-f1.csv --lead 5: the same as-of export as the DNF calibration row (829 rows, 40 races 2025-01 to 2026-16, gridq+pretrain+reset), 815 rows scored (10 DNS, 4 without a result), all 40 races with a forecast and a history row (history 147 races). Brier: model 0.1140 (se 0.0088), model-WX 0.1140 (se 0.0088); paired model-WX − model +0.00007 (se 0.00009; log loss 0.3884 → 0.3885). The scale is small: 0.961 to 1.064 across the 40 races (median 0.991), so the forecast barely moves a driver's price, and where it does it doesn't help. Same forecast in f1 props --check --history <history.csv> --forecast ... --lead 5 (64 races 2024-02 on): climatology-WX − climatology red flag +0.0005 (se 0.0029), safety car +0.0012 (se 0.0010), both not better, as in the backtest above |
— |
| 2026-10-07 | 3 | Provisional. New variant flpos (model.FL_FROM = "position" with FASTEST_LAP, off by default; no live config or profile turns it on): the fastest lap goes to one classified car, drawn with weight band rate(finishing position) × exp(−(race pace − quickest classified pace) / pace_scale) from sports/f1/fastest_lap.toml, so it follows the simulated result and the car. Band rates P1 36/108, P2 18/108, P3–8 38/648, P9+ 15/1042 (bands set by eye on the counts before any backtest, sparse tail pooled); pace_scale 0.0217 of a lap. Drawn on the fastest lap's side stream, so every other price is byte-identical. Used by combo pricing (markets/combos.py, Lab job f1_combo) |
A priori from history, not tuned on a backtest. VM laps/results, 108 Grands Prix 2022–26 (fastest non-deleted race lap): the winner set it 36 times (33 %), P2 18; 0 of 258 DNFs. pace_scale is the one fitted number: a conditional logit with log(band rate) as offset and each car's realized median-lap gap, MLE k = 1/pace_scale = 46 (se 15); NLL 306.8 uniform, 242.2 bands, 236.5 bands + pace. Free band weights + pace fit history better (225.6, scale 0.0077) but on the simulation's pre-race pace gave the winner the fastest lap only 20 % on Singapore 2026 (run 9652's paces and win odds, emulated); the chosen form gives 37 % there (P(FL given a win) 44 % Antonelli, 28 % Gasly; history 33 % 2022–26, 42 % 2025–26), against 11.8 % for fastlap (owner's Mac). Per-driver habits (2024–26 winner-sets-FL: Verstappen 7/18, Antonelli 5/8) not modelled. To score: fl_prob against Kalshi's resolved KXF1FASTLAP markets before any live use, with its own row |
fastlap (race pace + noise; the winner sets it far too rarely); position only (ignores the car: a midfield winner gets the leader's chance); free band weights + pace (fitted on realized pace, too weak on pre-race pace) |
| 2026-10-07 | 9 | Parity sprint C2: the season sweep runs for any sport whose schema has a [sweep] table (pipelines/season_sweep.py; racinglines strategy sweep nascar, motogp sweep; search jobs kind = "sweep"), on Polymarket, Kalshi and OG.com (markets/venue_replay.py EXCHANGES). NASCAR / MotoGP use their [replay] stages, kinds, group targets and pricing model; a priori, none tuned: the F1 taker and maker defaults, the stage-aware taker skips race eve ([sweep] late_stages), the maker stops quoting at 00:00 UTC on race day (quote_until_hours = 0, the race's start time isn't in the schema), one pricing per race seeded by the event key so a race prices the same alone or in its season. Owner: the earlier weekend-based backtest stays available as an explicit stage mode, the stages setting ("weekend" = fixed [replay] stages, one pricing per race; "sessions" = the session schedule), unset = the schema's [sweep] stages (F1 sessions, NASCAR / MotoGP weekend), so old results stay reproducible beside a session-aware path. Every sweep run's params now carry sport and venue; F1 pages read F1 sweeps only. OG.com's taker pays its schema's flat $0.02 per contract on the cost per share |
No result from these sweeps yet: nothing is chosen from their data. F1 is unchanged (goldens byte-identical); its sweep may now also take --venue og |
— |
| 2026-10-07 | 9 | Confirmed by the owner 2026-10-07 (staging first; prod after the staging review). GlobalModel for NASCAR and MotoGP: [sport] pricing_model in sports/nascar.toml and sports/motogp.toml is now racinglines.models.model_global:GlobalModel (the replay, the sport forecast, the season forecasts and the paper backfill all read it), at its a priori defaults (noise 0.8 strength units, half-life 240 days, prior weight 3, uncertainty 0.5, retirements on, team prior off; no [model.defaults], nothing tuned). The earlier models stay runnable as [sport] challengers (backtest walk-forward <sport> --model nascar_results | nascar_recent_form | motogp_results | motogp_recent_form). Every model now prices the event's start list (its entrants less DNS in the walk-forward; the race's stored sessions, else the last race's field, in the forecast), not everyone ever seen (core backlog C5); the season forecasts' "auto" noise in places applies only to a model whose noise is in places |
The 2026-10-07 algorithm A/B/C study (/mnt/project-files/research/algo-abc/REPORT.md and run/tables.md, not in the repo; 2025-26 races scored, G = GlobalModel at unbound defaults, A = the form model's price): NASCAR (74 races) win log loss G 3.1046 vs A 5.1841 (G minus A -2.0796 [-2.7217, -1.4657], better), head-to-head log loss G 0.6189 vs A 1.4958 (uniform 0.6931); MotoGP (37 races) win log loss G 2.2821 vs A 2.7363 (-0.4542 [-1.2097, +0.2179], noise), head-to-head G 0.5794 vs A 0.6266, top-10 Brier better (-0.0288 [-0.0467, -0.0106]). The form model fixes noise at 2.0 places and lets every car finish (C6). Not shown: the study priced every entrant seen, not the start list; pairwise Elo beat G on head-to-head in both sports; no market or P&L comparison. Changes every NASCAR/MotoGP price; F1 and downhill untouched |
[sport] pricing_model of the 2026-09-30 "NASCAR / MotoGP taker replay" entry (the sport's own model) |
| 2026-10-07 | 9 | Provisional, a priori, nothing tuned. NASCAR pole and fastest lap (parity sprint C12b): the GlobalModel draws side classifications for the same start list, named in sports/<sport>.toml [model]: sessions = ["qual"] (the qualifying order from past qual rounds, stage_rank["qual"], prices race_pole) and best_lap = "race_fastest_lap" (the order of each past race's best lap from the laps table, the car drawn first sets the fastest lap, prices race_fastest_lap and race_constructor_fastest_lap). Each is the race's structure on its own history: field-size-free strength z, recency half-life, prior weight, noise and uncertainty at the race's settings (240 days, 3, 0.8, 0.5), no retirements, no team or rating prior. Drawn on a side generator seeded from the shared one without advancing it (salt 7401), so every race price is unchanged. Settled from the stored data: the race's qual round (qual_position; a rained-out qualifying writes none: undecided) and the race's laps (a race_fastest_lap column, only when laps_complete). Their links are not filed as modeled yet ([markets] kinds unchanged: tests/test_classify_parity.py pins race_pole as unmodeled; the lead decides); MotoGP names sessions = ["qual"], inert until its qualifying is backfilled. The walk-forward now scores indicator kinds |
Kalshi lists KXNASCARPOLE (pole 0/12 races) and KXNASCARFASTLAP (fastest lap 0/21) for NASCAR (parity plan, 2026-10-07); 270 qualifying rounds and laps for 279 races are stored (same plan). Not shown: no walk-forward or market score of either kind on real data yet (the sandbox has no NASCAR database); the settings are the race's, not fitted to qualifying or lap times; whether Kalshi settles a rained-out qualifying on the formula grid is unknown |
— |
| 2026-10-07 | 9 | Provisional, off by default. F1 practice fastest (parity sprint C12b): race_fp1_fastest, race_fp2_fastest, race_fp3_fastest as declarative rows of markets/kinds.toml (stage_top n = 1 on stage_rank["fp<n>"], settled on fp<n>_position). position_sim draws each practice session's order of best laps behind the practicefast variant (model.PRACTICE_FASTEST, default False): qp plus the qualifying noise (sigma_q, rho_q) plus PRACTICE_SIGMA = 0.007 of independent practice noise, on a side stream (salt PRACTICE_SEED 20261007 + session index), the real order once the session has ended. Settled from the stored laps' order of best non-deleted laps (practice.classification for the model frames, practice.outcomes for the database; not yet merged into private_book.race_outcomes, C5). Walk-forward: practice.score prices each session one minute before it starts |
Polymarket lists 526 / 344 / 368 "Practice N fastest lap" links (parity plan, 2026-10-07). 0.007 is the within-session spread of (practice best-lap deficit minus qualifying deficit) on the 10 fixture weekends (tests/fixtures/f1, 538 driver-sessions: FP1 0.73 %, FP2 0.73 %, FP3 0.57 %), set once, not searched. practice.score on the same fixtures (2,000 sims, seed 0, 23 sessions over 9 weekends; the first has no history): mean log loss of the fastest driver 1.985 vs uniform 3.085 (FP1 2.058, FP2 1.878, FP3 1.997). Not shown: in-sample (the sigma comes from the same weekends); no market comparison; Sprint Qualifying is not a practice-fastest market (it is the sprint pole); every golden and default price unchanged (variant off) |
— |
| 2026-10-07 | 9 | Parity sprint C5, settlement parity. One top-n rule: a top-n market is YES when the driver is classified and placed in the top n, where who is classified is a schema field, [results] classified in sports/<code>.toml: status_ok (results status OK; the default, F1 and MotoGP) or placed (any car with a position; NASCAR, whose official order ranks every car, DNFs by laps run). The NASCAR value is inferred from its feed (sources/nascar/ingest.py), not verified against any venue's rule text. The rule is applied (markets/payoffs.top_n, used by kinds.settle, the declarative top / position predicates, the walk-forward, position_replay and season_sweep (both now settle through kinds.settle), the private book, venue_replay and the maker replay). The loaders (private_book.race_outcomes, position_replay.race_results, the walk-forward) mark each results frame with the sport's rule, so the walk-forward and the replay agree for every sport; an unmarked frame is status_ok everywhere except position_replay.settle, which keeps its earlier placed rule for frames not loaded through race_results. A head-to-head goes by position, a driver with no position behind one with a position (the walk-forward said NO there, the replay YES: now YES), neither placed NO (the walk-forward's; the replay left it undecided). Biggest mover settles from the race round's stored grid (results.extra.grid) by the 2026-10-04 rule; undecided without a stored slot or when a pit-lane starter could be the mover. racinglines markets --exchange X settle-check (and --exchange og settle --check) lists every link where our outcome differs from the exchange's resolved_yes |
No venue's top-n, head-to-head or biggest-mover rule text is recorded in the repo (Kalshi keeps it on each link as params.rules, on the VM only), so both values are unverified: owner to confirm against Kalshi's and OG.com's NASCAR top-n rules (does a DNF car's place count?) and how venues count a pit-lane start. Not a tuned setting. F1's walk-forward, private-book and weekend-sweep top-n settlement is unchanged (it already was this rule); NASCAR replays and sweeps keep counting a DNF by its place, while NASCAR's walk-forward now does too. NASCAR's GlobalModel still prices status OK (finished), which doesn't match placed: that's left for a model change |
the replay's own settlement (2026-09-30 entry: "the result settles by classified position") |
| 2026-10-07 | 9 | NASCAR and MotoGP session schedules (parity sprint C12a): sports/nascar.toml and sports/motogp.toml gain [sessions] (time_key: the rounds.extra key with a session's UTC start, run_date_utc / session_date), [sessions.schedule] by round kind and [stages], so the season sweep's stages = "sessions" trades after each stored session through core/stages.py. A priori, none tuned: session minutes (NASCAR practice 50, qualifying 60; MotoGP FP1-FP3 45, FP4 30, Practice 60, Q1/Q2 15, sprint 45, warm-up 10), F1's pre_minutes 60 and lag_minutes 30, until_fallback_hours = 0 (00:00 UTC on race day where the race round stores no start: every NASCAR race, the MotoGP races ingested without sessions), and [sweep] late_stages "after Q" (NASCAR) and "after Q2", "after Sprint", "after WUP" (MotoGP) for the stage-aware taker in this mode. An event with no stored session time keeps its [replay] stages. The pricing model is asked at every stage with the start list and the sessions run so far, from the event's own seed; GlobalModel reads neither, so its fair is the same at every stage. Both modes now price the start list (entrants less DNS, as the model's walk-forward does) |
Owner, 2026-10-07: every sport reprices after each of its sessions through the same stage engine. Stored times (read-only query, 2026-10-07): NASCAR 206 of 283 fp1 and 200 of 270 qual rounds carry run_date_utc (2019-); MotoGP 34 of 202 events carry session_date on every session; no MotoGP qualifying round is stored (the API's type "Q" with number 1 / 2 isn't mapped by session_round_kind). NASCAR stage 1 / stage 2 are in-race (no round, no time) and are not stages. The default stays stages = "weekend": its output on the same data is byte-identical to the base commit's (tests/test_season_sweep_sessions.py) except on races with a DNS entrant (112 NASCAR race rows on 2026-10-07), whose field now leaves the non-starter out. No result from a sessions sweep yet: nothing is chosen from its data |
— |
| 2026-10-08 | 9 | Provisional, deliberately wide. NASCAR and MotoGP group coherence tolerance on Polymarket: [replay] coherence_tol = { polymarket = 2.0 } in sports/nascar.toml and sports/motogp.toml, read by position_replay.coherence_tol for the replay, the season sweep and the paper backfill (sport_signals); every other venue keeps COHERENCE_TOL = 0.25, and a coherence_tol_by_kind sweep setting still overrides it per kind. F1's pipelines don't read it |
Owner, 2026-10-08: "keep them wide to start. better to see a bad strategy than not running our new data through backtests / p&l history pipeline." At 0.25 no NASCAR Polymarket race traded in the C8 sweeps (runs 9686, 9695: $0 and $55): a 40-car winner field's prices sum far above 1 on long shots with near-empty books (read-only on prod 2026-10-08: Daytona 500 2026, 42 drivers, 1.99 at 14 Feb 12:00Z; Hollywood Casino 400 7.73 on average over its 135 stored hours). 2.0 is the setting's maximum, not tuned; the $50 24 h volume floor still keeps dead markets out. Not shown yet: any P&L at 2.0; whether a long-shot-heavy field at 2.0 trades against stale midpoints | COHERENCE_TOL 0.25 for these sports on Polymarket (2026-09-30 "NASCAR / MotoGP taker replay") |
| 2026-10-08 | 9 | Provisional, opt-in. NASCAR "race_day" stage mode: [replay] race_day_stages = [["race morning", 12]] (12:00 UTC on race day) after the weekend stages, the maker quoting until [sweep] race_day_quote_until_hours = 15, and "race morning" a late stage for the stage-aware taker. Picked with --stages race_day (replay, demo-history) or the sweep setting stages=race_day; the default stays "weekend", so earlier results reproduce (owner rule 2026-10-07; test_the_weekend_mode_reproduces_the_base_commit unchanged) |
Polymarket's NASCAR volume comes on race day, after the last weekend stage (race eve, 18:00 UTC the day before). scripts/vm/why_untradeable.py on staging, 2026-10-08, PROBE_HOURS=-6,6,12,15,16: Window World 450 2026 had 0 markets over the $50 floor at race eve and 31 at 12:00 UTC (price sum 1.88); Iowa Corn 350 and Cook Out 400 turned coherent only on race day. 2025 markets read as closed on race day (open 0 from 06:00 or 12:00 UTC), so the stage adds nothing there. 12:00 UTC (8 am ET) is assumed before every Cup start: an inference, since the race start isn't in the [replay] schema; not checked against stored race start times. Not tuned; not shown yet: any P&L, and whether 15:00 UTC is safe for the maker on every race |
No race-day stage for NASCAR (2026-09-30 "NASCAR / MotoGP taker replay") |
| 2026-10-11 | 9 | Provisional, staging only. Prod changes wait for the owner's go. "One engine": racinglines.models.model_global:GlobalModel is the only [sport] pricing_model of every modelled sport (F1, downhill, NASCAR, MotoGP; model_family = "global"); tape-only sports stay "none". Each schema declares the engine's inputs, [model] inputs (F1: results, qualifying, grid, practice, laps, sectors; downhill: run_times, practice; NASCAR: results, qualifying, fastest_lap; MotoGP: results, qualifying), and its per-sport defaults, [model.defaults] (F1's and downhill's settings as they were: variant baseline, sims 4000, half-life 120 d, ridge_team 2.0, ridge_slope 6.0, driver_prior_n 3.0, reset_weight 0.25, ...; downhill category ME, sims 5000, half-life 240 d, prior_n 0.5, junior_weight 0.25). The inputs pick the core (models/inputs/__init__.py core_for: results, laps or run_times), never the sport's name; an optional input left undeclared turns its setting off (sectors → track_features, practice → practice_prior). The F1 code moved to input-named modules (models/inputs/laps.py, laps_pricing.py, practice.py, grid.py, laps_core.py), downhill's to models/inputs/run_times/, the results model to models/inputs/results.py; position_sim and timed_runs are deprecated import aliases for one version. model_runs.model stays f1_sector_sim / timed_runs / global_results ([model] name). The old engines stay runnable as [sport] challengers (f1_sector_sim, timed_runs); --model global is the engine at the results level |
Owner, 2026-10-11: "Single global model. Set it up/ preserve settings there based on original f1-specific engine. For ALL sports, we will want to be able to handle high-level and/ or granular data." Meant to be price-preserving: the cores keep their algorithms, so with the same seed and data every price is unchanged. Checked on synthetic data (tests/test_one_engine.py): F1 engine = PositionSim = price_race array for array (variants baseline and gridq+pretrain+reset, sprint weekends included), downhill engine = TimedRuns, NASCAR and MotoGP = the counts the 2b7b410 GlobalModel gave; 207 OutcomeSims arrays dumped from 2b7b410 and this change are identical; F1 settings keys unchanged (c107835cbced, model key 56f55ac79102). Not shown: the golden suite (needs tests/fixtures/, owner's Mac), a database run, prod. Nothing tuned; blending laps into NASCAR and MotoGP is phase 2, after validation |
Per-sport pricing models: race_model:PositionSim (F1), race_model:TimedRuns (downhill) as pricing_model |
| 2026-10-12 | 8 | Defaults sweep (sweeps/defaults-2026-10.toml on staging, 152 jobs, 46 at 16k; report library/core/2026-10-12-defaults-sweep-report.md): A stays the core taker; no settings change; the why numbers of A, T1-T10 and TB are refreshed to 16k on 16 + 23 weekends. min_volume_24h=25 on the stage-aware taker is held pending a seed check (owner card, 2026-10-12) |
Held-out rule, noise floor measured tonight (16k, 3 seeds): update $208, stage-aware $150 (configured). Stage-aware at a $25 floor: +438 (2026) / +211 (2025) vs T8, but −26 / −14 without the two best weekends of each season and under the 2026-09-30 ±700 floor; update at $25: +165 / +375, inside noise in 2026. T3 and T4 drifted (2025 −114, −40) for an unknown reason | — |
| 2026-10-12 | 8 | Seed check passed (owner chose "seed check first", 05:18Z). T8, the stage-aware taker, takes a $25 24 h volume floor: settings=dict(_T1, min_volume_24h=25), renamed "T8 · stage-aware (early, $25 volume floor)" so a new Lab candidate is made; TB inherits it. A and the update takers keep the $50 floor. Accounts already on T8 or TB keep their saved settings until they pick again |
sweeps/seedcheck-2026-10.toml on staging (14 jobs, 0 failed, runs 10833-10846): stage-aware P&L at a $25 floor minus T8 at $50, same seed, 16k: 2026 +438 / +473 / +475, 2025 +211 / +233 / +224 (default seed, seeds 1, 2), above the $150 noise floor in both seasons at every seed. Weekends better / worse: 2026 4 / 3, 2025 7-8 / 2. Narrow: without each season's two best weekends the gain is -26 to +5. The update taker at $25 (+161 to +203 in 2026) stays inside its $208 floor, so A is unchanged |
T8's $50 floor (taker re-sweep, 2026-09-30) |
| 2026-10-12 | — | Owner decision (decision card, 19:47Z): "Size followed strategies to the player's F$1,000". A registered fantasy player's followed taker strategy is re-sized per player in the live signal pass (fantasy/sizing.py, signals.for_player): strategy bankroll = the player's grant (F$1,000) + the strategy's own paper P&L on every other event (settled at the outcome, open at the mark; the event being traded is left out so its own marks never resize it); stakes x bankroll_scale(grant, bankroll) (1.0 at F$1,000, 1.5 after +500, 0 once it is gone, so a busted strategy stops trading; a blend member's weight stays multiplied in); max_deployed = the bankroll less the cost still held in its unsettled positions on other events (x a blend member's weight), enforced across the weekend's markets in time order by the backtest's cap (taker_weekend._capped, now used live through taker_signals(capped=True)). One shared F$1,000 (owner, 21:04Z: "Only 1k to use for both"): the bankroll also adds the player's hand-trade P&L and ties up the cost of their open hand trades, and hand trades can spend only cash the strategy hasn't tied up (ledger.available) |
"Stakes scale with the balance and total spend is capped, so strategy and manual P&L compare fairly" (the card's consequence). Unchanged: the demo accounts (maker, taker), accounts without a grant, maker strategies, backtests, sweeps, replays, the paper backfill, golden tests and the Markets page's per-profile calls; the shared computation is still one per profile, only the per-player copy is re-run |
Fixed per-profile stakes for players (stake_per_edge 250, max_stake 50, no cap) |
| 2026-10-12 | — | Owner decision (21:04Z): one user level for the alpha. "Basic can get everything ... re-merge the two user levels, only use pro for fantasy, and think about the feature segregation later." Under the alpha layout a basic account runs as pro on every request (web/layout.role), so everyone gets the Lab, the full picker and the tweaks; users.role is not rewritten |
Reversible with the layout switch; no data step | Basic accounts limited to the five styles with no tweaks |
| 2026-10-12 | — | Owner request (19:52Z, 19:53Z): Kelly sizing is a player option. A registered player's followed strategy is sized "standard" (the row above; the default) or "kelly" with a fraction from 0.10 to 1.00, stored on their assigned profile (users.prefs profile keys sizing = "standard" |
"kelly" and kelly; profiles.sizing_of / set_sizing; anything else, e.g. 0.05 or 1.5, is standard). Kelly reuses the existing taker Kelly sizing (TakerParams.kelly, bankroll; sizing.target_shares: stake = kelly x f* x bankroll) with bankroll = the strategy bankroll (grant + the strategy's own P&L on other events), the per-market cap max_stake x bankroll / grant and the same max_deployed cap |
"Expose some dynamic sizing options like half or quarter kelly. Since I was doing that regularly during my first live week of sports book trades." No profile's own settings change (no candidate gets a kelly default); demo accounts, backtests, sweeps and replays never read it. At the current max_stake (50) Kelly stakes are usually capped per market, so the choice mostly changes small-edge trades |
| 2026-10-12 | 9 | Provisional, a priori, nothing tuned; admin only. Road cycling on the one engine: sports/road_cycling.toml [sport] model_family = "global", pricing_model = "racinglines.models.model_global:GlobalModel", [model] inputs = ["results"], source = "pcs_road" (ProCyclingStats one-day races and stage-race final GCs, loaded by racinglines data ingest cycling, racinglines/sources/pcs_ingest.py; ITTs stored as pcs_itt, outside the history), group = "team" (inert: PCS results carry no team). [model.defaults], every value a priori: sims = 4000 (fields of 100-180 riders), half_life_days = 365 (the book model's road guess; riders race fewer days than drivers), prior_weight = 3.0, uncertainty = 0.5, dnf = true (the engine's defaults), noise = 1.0 (wider than the motor sports' 0.8, so favourites are not over-confident before any calibration). [forecast] field = "entry_list" (only a race with its own start list, racinglines data schedule cycling, is priced) and h2h = true (every pair's head-to-head stored in race_predictions.extra.h2h): new optional keys of pipelines/sport_forecast.py, off for NASCAR and MotoGP. Board columns race_win, race_podium, race_top10; calibration = "baseline"; audience = "admin" until the owner's data-terms review of ProCyclingStats (reviewed for personal, non-commercial use 2026-10-05). The book model (models/cycling.py, racinglines model price cycling) is kept as the sportsbook tool, not a challengers entry (no race_model contract) |
Owner: road cycling on the Markets page through the one global engine, sports differing only by schema (no sport-specific model on the board). Not shown: a walk-forward on the real PCS history (the files are on the Mac; racinglines backtest walk-forward road_cycling once ingested), a database run, staging. Open: whether stage-race GCs belong in a one-day race's history at full weight (the book model weights them 0.5), and whether the GC page's date is the race's last day (an earlier date would leak a GC into a backtest of races during the stage race) |
model_family = "none" (tape only) for road cycling |
| 2026-10-14 | 8 | Optional, off by default, nothing chosen. The root unit system (owner, 19:05Z, Circles Off ep. 181, Dan Abrams: fitted to a UK analyst's study of about 50,000 soccer matches, named "Joseph Bookall" in the owner's summary: attainable edge scales with the square root of the odds): the sweep setting root_unit ($) sizes a taker's stake as root_unit × √(c / (1 − c)) for a contract costing c, in place of stake per unit edge: 1 unit at evens, 0.5 at 4/1, 2 at 1/4, still capped at max_stake and scaled by bankroll like the linear rule; refused together with kelly. Off (unset) keeps every combo key and result unchanged (tests/test_sizing.py). Choosing it waits for a sweep against the default and Kelly on P&L and max drawdown |
Synthetic tests only; no sweep run yet | — |
| 2026-10-14 | 8 | Optional, off by default, nothing chosen. Square-root bankroll growth (owner's first handoff, 19:01Z; the episode's rule is the odds-based row above): the sweep setting bankroll_growth = linear (unset; today's balance / bankroll) or sqrt (its square root, so stake = C × √balance × edge with C = stake_per_edge / √bankroll). With bankroll set, a quadrupled balance doubles the stakes and one down 75% halves them; with kelly, Kelly sizes on √(balance × bankroll). It is a Setting, so the CLI (--bankroll-growth), the Lab sweep form and MCP run_job settings all take it. Unset keeps every combo key and result unchanged (tests/test_sizing.py). Choosing it waits for a sweep that compares it with linear and Kelly on P&L and max drawdown |
Synthetic tests only; no sweep run yet | — |
| 2026-10-14 | 8 | Optional, off by default, nothing chosen. Calibration power on the race-win prices (C48): the sweep setting calib_power maps win chances to p^a (renormalised) and resamples the sims so every market follows. walk_forward reads sports/f1/calibration.toml, fitted on earlier races only (grid 2.3, nogrid 2.1 after 2026-15; leak check: 31 cut points, no earlier row changed). 4,000-sim sweeps: race-win log loss 0.1694 → 0.1627 (2025), 0.2244 → 0.2194 (2026, market 0.2222); all 9 strategies' P&L better in both seasons (F1 calibration) |
The model is flat: 2025 drivers priced 5–10% won 2.0%. One power keeps podium, h2h and constructor prices coherent with the win price. Not the default: the base settings and the method were chosen with these seasons in view; promotion is the owner's call | — |
| 2026-10-15 | 8 | Per-event lock for paper trading, round 18 to the end of 2026. One strategy is committed before each event's first session; additions only after a stage, each timestamped at the price then; nothing edited; scored as bet, sportsbook lines included (Paper trading) | Owner, 2026-10-15: "everything must be done as bet, as if the paper trades were a live test". Tonight's golden and walk-forward runs are in-sample (profile A was tuned on 2026); only a pre-registered weekend is out of sample | Within a weekend, 2026-10-05's "profiles stay tunable" (they still change between weekends) |
| 2026-10-14 | 6 | Provisional, confirmed by the owner 2026-10-14 (staging first). F1-6 starts, narrowed. One Plackett-Luce model, racinglines/models/rank_logit/ (a [sport] challengers entry, --model rank_logit, every default price unchanged): one Davidson-Luce likelihood over rankings of blocks of entries, plain Plackett-Luce the special case. Variation is settings and schema, never a second class: [model.rank_logit.rankings] names the round kinds that become rankings and their weights (race, sprint, qual, stage, GC), tie_on = "time_ms", top_k (only the first k places are chosen, the rest an unordered tail still in every denominator), team_effects, team entries (sum of member worths, prices race_top_constructor consistently), kind_sd (athlete × round-kind offset, 0 = off), the priors and half_life_days; a Laplace covariance is the per-market uncertainty input for F1-4. Defaults reproduce the 6–7 Oct fits (no ties, no teams, no truncation), so the comparison switches one extension on at a time, paired ±2 SE (E5 eval compare), on win, podium, top 10 and head-to-head, plus race_pole from NASCAR qualifying and the downhill Final. Ported from the owner's pl_ext.py prototype (brute-force likelihood and gradient checks pass). No pltree, no PyMC/NumPyro, no Weng-Lin/Elo (research only) |
Plain Plackett-Luce lost the win market twice: 6 Oct model_pl (Hunter MM, half-life 240 d, prior_weight 3; win log loss F1 0.1556 vs 0.1476, NASCAR 0.1151 vs 0.1085, MotoGP 0.1620 vs 0.1608, no SEs; Novig and the bake-off, Part 3, restored from commit 2c8bcb10) and 7 Oct research/algo_abc (MAP, half-life 365 d, N(0, 1.5²) prior; worse on race win in all four sports, 95% CIs exclude 0; engine roadmap E7). Both already had a prior and time decay; neither had ties, team effects, team entries, top_k or a covariance. Ties occur where they would be priced (results with a time, 2026-10-14): NASCAR qualifying 122 of 271 rounds (largest 3), F1 qualifying 7 of 147, downhill 1–4 rounds per kind, races none. The win loss has a named mechanism (every place counts the same, one fixed Gumbel spread) that only top_k addresses; a rerun without it is expected to lose again. One model, not a library (owner, 2026-10-14): the one-engine rule (2026-10-11, C56) |
Bake-off doc "not built (E7)"; F1-6 "PyMC or NumPyro" |
| 2026-10-15 | 8 | Local research only, no promotion. Finite-pool Jev auto-sweeper: typed Choice picks a full settings id or STOP; deterministic Python deduplicates exact (year, Settings.key) runs and enforces cumulative sweep/API budgets, both initially zero. Selection objective is the existing search-report baseline-gain branch: target 2026 gain + min(0, held-out 2025 gain), with ordinary verdict and higher-simulation confirmation evidence displayed separately. Controller noise floors stay at the report defaults ($150 taker / $350 maker); descriptive reports can measure replicate noise and differ. Default strategy update, confirmation 16,000 sims, stop rule off; these are operational defaults, not tuned winners. No profile, paper lock or trading setting changes |
Owner selected existing target/held-out search-report evidence rather than raw P&L and approved the interruptible, warm-started local MVP. The finite supplied pool and existing season sweeps keep Jev inside a bounded decision space; automatic candidate generation, other venues and MCP/Lab execution are deferred. Fixed controller noise floors keep cached verdicts stable, but their adequacy is unverified. A score is not a promotion or a promise of profit; no real run has validated this controller yet. See Local auto-sweeper | — |
| 2026-10-16 | 8 | Not adopted: failed its 2025 holdout (read 2026-10-17); stays an option, unset by default. The taker sale floor (sell_floor, a Setting, unset = none): edge lets a rebalancing sale fill only at or above fair − min edge, net of cost and fee, for every taker_fill. It is the exit rule, not book depth, that carried the book walk's gain (#219). No default changes until sweeps/holdout-2025-sellfloor.toml (#228, 2025 rounds 22-24) is read: kept if the floor is no worse than the holdout without it (ours −71, Probalytics ask −37) on both tapes. Holdout result (holdout-2025-sellfloor, mean of 3 seeds, update): ours + floor −90 (−71 without), Probalytics ask + floor −72 (−37 without), worse on both tapes; 21-22 trades vs 26-27; hold identical (no sales), so the run is clean. Three weekends of a season with no edge out of sample: the floor's in-sample gain is not shown to hold |
golden-probalytics-sellfloor-2026 on staging (profile A, min edge 0.10, Probalytics tape, update, mean of 3 seeds): touch +1,415 → touch + floor +2,031 on Polymarket, +25 → +1,470 on Kalshi (rounds 5-17); book + floor (#219) +1,918 / +1,607, within the ±100-200 seed spread of touch + floor, so depth is about neutral at max_stake $50; book with no floor +1,795 / +806. hold (no sales) unchanged by the floor (Kalshi +1,760 vs +1,751). In-sample: profile A was tuned on 2026 and the rule was chosen after the losses were seen |
No floor (sales at any bid) |
| 2026-10-17 | 8 | Calibration stays off in every per-event lock, rounds 18–23 (calib_power unset; the 2026-10-14 row's setting is not chosen). Retuning later counts as a new entry from its own date (2026-10-15 lock row) |
The only out-of-sample check goes the other way. 2025 holdout (Las Vegas, Qatar, Abu Dhabi; Polymarket; profile A update, Probalytics at the ask): −37 off vs −95 on; maker −228 vs −516 (fill-fidelity report, library engineering/2026-10-16-fill-fidelity/report.md §5). NASCAR's fit (Phoenix, run calibration-20261016T211903Z) also stays off: 2026 race-win log loss 0.2358/0.2370 vs Kalshi 0.2089. The in-sample gains (2026-10-14 row) were measured on the seasons the method was chosen on. Owner approved in the "pre-lock improvements" thread, 2026-10-17 04:56Z |
— |
| 2026-10-17 | 8 | Provisional, in-sample only. The locked taker's scoring settings include max_spread = 0.10: a stage whose quoted spread is wider than 10¢ is not traded. It acts on Kalshi, and on Polymarket only with taker_fill = touch on Probalytics' tape (pipelines/sweep_settings.py). The live signal pass applies it and sell_floor from the profile's settings like the sweep (pipelines/signals.py taker_params, owner chose "wire into live", 2026-10-17 05:25Z); taker_fill stays sweep-only (live prices from our own tape) |
It added $126 to $254 wherever it ran on profile A update: Polymarket Probalytics +1,415 → +1,595, Kalshi Probalytics +25 → +151, Kalshi ours +1,096 → +1,350 (report §3). Not in the 2025 holdout (no Kalshi there, and the holdout queue doesn't set it) |
— |
| 2026-10-19 | 9 | Owner decision (decision card, 19:39Z): "Go + rerun jobs". The old F1 and downhill engine classes are retired: race_model.PositionSim and race_model.TimedRuns, their [sport] challengers entries (--model f1_sector_sim, --model timed_runs) and the deprecated import aliases racinglines.models.position_sim and racinglines.models.timed_runs. The laps and run-times cores stay the F1 and downhill engine; model_runs.model stays f1_sector_sim / timed_runs (history keys). Retiring the cores themselves (pricing F1 and downhill from the results core) is a separate gate, no-go until the laps-vs-results walk-forward comparison (Lab jobs 14 and 15) and the parity inputs meet the owner's tolerance |
Price-preserving cleanup: the classes were the same cores with the same code settings, so they priced identically to the engine (tests/test_one_engine.py still checks engine == core with the code settings == laps_pricing.price_race). The aliases were promised to go "in the next version" (2026-10-11). Report: internal book, "Decision gate: fully retiring the old F1 and downhill models". Not shown here: the golden suite (owner's Mac) |
The 2026-10-11 "One engine" row's challengers and aliases |
| 2026-10-21 | 8 | Owner decision (decision card, 02:03Z): "Open the PR". F1's calib_power default is walk_forward (pipelines/sweep_settings.py, models/inputs/laps.py), so live prices, Lab runs and the Austin lock (round 18) are calibrated unless a run names calib_power = off. off keeps every key it had before (the 2026-10-11 pins, e.g. c107835cbced), and a saved run without the setting reads back as off. The table is sports/f1/calibration.toml as committed (fits through 2026-15). NASCAR and MotoGP (results core) stay off. Golden suite on the owner's Mac owed before main |
Owner, 2026-10-20 22:52Z: "Force the runs to use calibration"; 2026-10-21 02:00Z: "lock calibration ahead of Austin cutoff". Staging sweeps 2026-10-21 (demo DB, Polymarket, gridq+pretrain+reset; library core/2026-10-20-f1-recent-form-sweeps.md): 2026 after qualifying win Brier −0.0065 vs market (off +0.0025), held-out R13-17 CLV −0.19 / +0.51 update / early (off −0.99 / −0.18), top pick priced 0.668, won 0.706; 2025 win Brier +0.0009 worse before and after qualifying, CLV +1.30 (off +0.99). Against it: the 2026-10-17 row's 2025 holdout P&L (−95 on vs −37 off), and no luck test (golden rule 1). Prod's calibrated runs on main don't match the staging numbers (cause not checked) |
The 2026-10-17 row "Calibration stays off in every per-event lock, rounds 18–23"; the 2026-10-14 row's "off by default" |
| 2026-10-21 | 8 | Owner rule (02:42Z): the hosted default and the package default split. walk_forward stays F1's default on the hosted service (racinglines.bet, MCP, remote CLI, VM jobs), read from racinglines/hosted.py; the PyPI package leaves that module out (oss/manifest.toml), so calib_power defaults to off there (models/calibration.py DEFAULT). The setting stays in the package, documented only in its help text |
Owner: "FOR USERS THAT DOWNLOAD THE PACKAGE: Leave the calibration flag off, keep it buried somewhere but minimally documented." tests/test_calib_power.py::test_the_package_default_is_off |
The package shipping walk_forward |
| 2026-10-25 | — | Owner decision (20:52Z, Austin): no per-market stake cap for a player's followed strategy. "lift all per market sizing caps, let the sizing be determined strictly from: bankroll size when bets are placed, percieved edge per bet, and the kelly sizing defined by the user's strategy". fantasy/sizing.sized sets max_stake to the grant, so a stake is limited only by the strategy bankroll (max_deployed); standard sizing stays stake_per_edge × edge × bankroll scale, Kelly stays kelly × f* × bankroll. Applied to Austin (2026-18) on prod as a hotfix before its results settle, so Austin's booked strategy trades re-size |
The profile default max_stake $50 (sweep_settings.py:96) capped nearly every Austin trade, so every player on a style held identical stakes whatever their sizing (duvadu4616 on half Kelly matched the standard Steady accounts). Backtests, sweeps, the Lab, the demo accounts and the Markets page's calls keep the profile's own max_stake |
max_stake $50 per market for players (the 2026-10-12 rows) |
| 2026-10-25 | — | Owner decision (21:03Z-21:04Z, "lgtm"): the fantasy leaderboard score is ROI × (trades ÷ 10)^0.3; likely to stay for the fantasy alpha unless major issues are found (owner, 21:04Z). "ROI is primary figure of merit, trade volume adds a gradual boost. 1000 trades significantly better (2x?) than 100 trades and same ROI". fantasy/ledger.score (VOLUME_BASE 10, VOLUME_POWER 0.3): ×1 at 10 trades, ×2 at 100, ×4 at 1,000. MIN_TRADES stays 5 (a 10-trade minimum would leave the board empty after Austin, where every player has 2 to 5 trades); the History page, the MCP leaderboard tool and docs/alpha-layout.md say the same |
The owner's 2026-10-12 rule was "made up and it isn't good" (21:02Z): with √trades, volume outweighed ROI (100 trades at +5% scored 0.50 against 9 trades at +15% at 0.45). log₁₀(trades) was considered and rejected: 1,000 trades scored only 1.5× 100 | ROI × √trades (2026-10-12) |
Open questions¶
- Do live paper fills match the backtest? The replay's "through" rule is conservative, but live queues and competing makers may fill less (F1-8).
- How much to size up after validation, and on what evidence (A-lite → A)?
- Kalshi's F1 depth and resolution rules: enough to quote, or only to take (F1-9)?
- Are the search's noise ranges real? The Monte Carlo seed is fixed, so noise replicates aren't independent until a seed setting exists.
- Sprint weekends: does the F1-2 stage-specific training need separate handling for sprint-format stages?
- Should maker spreads come from per-market model uncertainty (F1-6) or from measured markouts (F1-4)? F1-4 showed markout-driven widening doesn't help on 2026; live markouts from F1-8 are the next evidence.
- Which sport next? F1 and UCI downhill are the endpoints for audience and expected liquidity; candidates in between are in Roadmap.
Collaborators and beta testers¶
We're looking for beta testers and collaborators! Try the app at racinglines.bet, and if you'd like to help, test, or just say hi, drop us a line at [email protected]. We'd love to hear from you. The contributor guide says how to get set up.