Overnight VM run¶
scripts/vm/overnight.sh: the long F1 searches, the NASCAR and MotoGP tape replays and the demo taker's story, run on the VM as one transient unit with a progress line every 5 minutes.
Historical plan, current script (2026-10-14)
The plan below is the one written before the first run on 2026-09-30 and kept as the record, with the changes that run forced folded in. The script and its modes (What runs on the VM) are current and are still how a long job runs on the VM: as a transient systemd unit, never in an SSH shell, backup first, with the progress lines of Progress updates. The 3 × 3 matrix it set out to fill is tracked on Sports and exchanges now.
Status: ran on 2026-09-30, in four starts. The full results are in the report,
reports/2026-09-30-overnight-vm-run/report.md; the data it wrote is in Data changes. The plan
below is kept as it was written before the run, with the changes the run forced (the per-sport preflight, the progress
lines, the MotoGP load order) folded into the sections they changed. The command sequence is in the Copilot handoff
handoffs/2026-09-30-overnight-vm-run.md in the project files (local only, never committed).
What actually ran¶
On the VM, resized to an n2-standard-2 for the night. It took four starts; the report names three stops, each fixed by a PR:
| Start | Stopped at | Why | Fix |
|---|---|---|---|
| 1 | NASCAR | The VM had 13,599 NASCAR Kalshi links but no NASCAR results, because nascar ingest and nascar link --apply had only ever run on the Mac |
#98: a per-sport preflight; the replay mode loads a sport's results and identifies its links when they are missing |
| 2 | MotoGP's results load | motogp ingest stops at the first error, and the older seasons had never been ingested anywhere |
#100: newest seasons first (2025-2026, then 2016-2024); a sport's failed data load is logged, not fatal, and the preflight decides |
| 3 | MotoGP grid ranking | 2025 had no MotoGP race with Kalshi markets, so its summaries had no update totals (KeyError 'net') |
#102 (merged): the ranking copes with a season with no races |
What it produced (the VM's own output, pasted 2026-09-30 16:07Z; details in the report):
| Piece | Result |
|---|---|
| F1 broad sweep at 4k | 104 of 106 jobs (2 failed) |
| F1 top 10 at 16k | 24 of 24. Profile A stays: no setting beat it by more than the noise floor (±352) in both seasons |
| Demo taker story | The gate passed; the taker's backfill was rebuilt: 2025 23 weekends +$450.46, 2026 15 weekends +$353.01 (paper) |
| NASCAR × Kalshi | Settings grid: 2026 +$4,226 to +$5,479, 2025 −$242 to −$390 in every setting. Not robust (2026-specific); nothing tuned, the defaults stay |
| MotoGP × Kalshi | Tape pulled, replay saves stored, 16 grid runs done; the ranking crashed before #102 (no grid.md from the run) |
| NASCAR champion forecast | data/runs/nascar-season-2026.csv written |
| OG.com buy-all (F1, NASCAR) | Ran; its output was not in the pasted tail, so unconfirmed. A plumbing check, never a result |
The absolute F1 totals do not match PR #85's (profile A +$2,607 in 2026 here against +$1,389 there); only comparisons
within this run are used, and the gap is unexplained. The decision-log entry is in the
F1 roadmap; the NASCAR and MotoGP grids later fed the demo paper portfolio (vm.sh demo,
Paper trading).
The plan (as written before the run)¶
It folds the F1 taker re-sweep (PR #85), the replay tape work and Mac Stage 1 (PR #84), the data-off-GitHub stages (PR #83, plan) and the standing backup and probe rules into one run on the VM.
The owner's asks (2026-09-30): a broad settings sweep, then a 16,000-simulation deep run on the top combos; F1 is the golden baseline; downhill and any sport without active markets since 2024 are out; NASCAR goes now and MotoGP once its diagnostic lands. The soft launch and fantasy test need a 3 sports × 3 exchanges matrix with no all-zero row or column. Long runs happen on the VM, for the record, after a small Mac/Copilot spot check; cloud sessions only write code, plans and handoffs.
The 3 × 3 matrix¶
Sports F1, NASCAR, MotoGP; exchanges Polymarket, Kalshi, OG.com. The other sports with markets (IndyCar on Kalshi, SailGP on OG.com, Le Mans) have no pricing model, and downhill has no exchange, so they are out.
A cell counts at one of three levels: - Replayed prices: the exchange's recorded prices replayed with a P&L, without our model (PR #88's buy-all, a debug check that the tape is real and settles). - Priced: our model prices at least one listed market on that exchange against a stored exchange price. This is what a fantasy pick needs. - Backtested: a replay or sweep over past races found tradeable markets and reports P&L.
| Polymarket | Kalshi | OG.com | |
|---|---|---|---|
| F1 | Backtested. Taker profile A, +$1,389 (2026) and +$1,432 (2025) at 16k (PR #85) | Backtested (maker). Profile K. The taker replay is not usable until Kalshi candles carry bid/ask (PR #85, section 3) | Priced. Drivers' and constructors' champion: og fair printed 20 F1 rows on the VM (2026-09-29). Shown in the app only with RACINGLINES_OG_VENUE=1, still off on the VM. No backtest yet. Replayed prices tonight with PR #88's buy-all on the VM's recorded OG.com prices |
| NASCAR | 0. 36 markets priced, 0 tradeable at a $0 floor (Mac Stage 1). Diagnostic 1d pending; likely a partial listing failing the coherence check | Backtested, one race. 178 tradeable at $0, 56 at $50 (Mac Stage 1, before PR #89, so some of those prices may be dead-book 0.50 midpoints). Needs the full tape pulled after #89 and the candle fix (tonight), and the spike check | Replayed prices tonight (PR #88's buy-all on the 17 Cup Champion contracts). Priced after the run (PR #93's season forecast: nascar season --quotes, read-only, the champion price beside OG.com's 17 contracts and Kalshi's; an indicator, not a trading input) |
| MotoGP | 0. Never synced | Priced, tradeable with PR #89. 22 priced and 0 tradeable before it. With #89 (dead-book 0.50 midpoints rejected, UUID rider ids kept), the Mac's replay shows 13 races, 288 priced, 186 tradeable. The 3 races still untradeable (USA, RSM, AUT) fail real liquidity or coherence gates | 0. OG.com lists no MotoGP that we know of (not in exchanges/og.toml, never probed) |
Where it stands: the MotoGP row is zero on the VM until PR #89 is merged and deployed. With it, MotoGP × Kalshi is tradeable on the Mac. After tonight, OG.com has two cells with replayed prices (F1 and NASCAR) and one priced by our model (F1). The OG.com column is zero for backtests, but F1 is priced there, so at the priced level the column is not zero once the venue switch is on.
The cheapest path to no zero row or column (the critical path):
| # | Cell | Level | What it takes | Where |
|---|---|---|---|---|
| M1 | F1 × Polymarket | backtested | done; tonight's golden check re-confirms it on the VM's data | VM, tonight |
| M2 | NASCAR × Kalshi | backtested | full tape pull, replay, spike check, settings grid | VM, tonight |
| M3 | F1 × OG.com | priced | turn on RACINGLINES_OG_VENUE=1 on the VM (display switch, not a trading flag), restart, smoke |
VM, owner's word |
| M4 | MotoGP × Kalshi | backtested | PR #89 and the candle fix merged and deployed, then tonight's replay with SPORTS="nascar motogp" (conditional: the dry run must show motogp_wc races and a Kalshi probe above 0) |
VM, tonight if #89 is in |
M1 to M4 give every row and every column at least one cell. After that, in order of cost:
| # | Cell | What it takes |
|---|---|---|
| N1 | NASCAR × Polymarket | the 1d diagnostic's NASCAR Polymarket line; if it is a partial listing, no coherence check for partial groups on that exchange (a schema line), then a re-run of the replay |
| N2 | MotoGP × Polymarket | the replay mode syncs MotoGP's Polymarket links when MotoGP is in SPORTS; then the same checks |
| N3 | F1 × OG.com, backtested | a Mac/Copilot probe of how far back OG.com's get-ticker-histories goes for the F1 champion contracts. The replay takes OG.com as a venue now (f1 season-strategy --venue og, on the stored tape; it reuses the as-of season forecasts the Polymarket replay stored, and stores any that are missing); the probe says how many decisions it can trade |
| N4 | F1 × Kalshi taker | re-pull Kalshi F1 candles with bid/ask on the Mac and fill the taker at the ask (PR #85, what's next) |
| N5 | NASCAR × OG.com | the Cup champion model is merged (#93); nascar season-replay --venue og backtests it on the stored champion prices (read-only, off by default, Championship markets). The OG.com tape starts about a month back, so few decisions trade |
What runs on the VM¶
scripts/vm/overnight.sh, as a transient systemd unit (rl-overnight), in one of five modes. It replaces the
rl-backfill script of the backfill handoff (revision 3) and drops its downhill walk-forward.
Step 1: the dry run¶
Read-only: counts, cores, memory and disk, each sport's tape probe and last-race replay, and the F1 golden check.
sudo systemd-run --unit=rl-overnight --uid=racinglines --setenv=MODE=dry /opt/racinglines/scripts/vm/overnight.sh
A sport with 0 events or 0 Kalshi links with a race is flagged NOT READY; the run refuses to start while a
racinglines-live-* unit is active.
Step 2: a writing mode, staging first¶
sudo systemd-run --unit=rl-overnight-staging --uid=racinglines --setenv=TARGET=staging --setenv=MODE=replay /opt/racinglines-staging/scripts/vm/overnight.sh
Step 3: follow it¶
Every writing mode backs up first and checks the dump's trailer. Log: data/runs/logs/overnight-<mode>-<UTC>.log;
markers overnight-<mode>.done / .failed; the progress lines are in overnight-progress.log
(Progress updates).
Staging. TARGET=staging runs any mode against staging instead of production. It reads
/etc/racinglines-staging.env, works in /opt/racinglines-staging, backs up and queries racinglines_staging
(data/backups/db/racinglines_staging-before-overnight-<mode>-<UTC>.sql.gz), and waits only for staging's own live
units (racinglines-staging-live-*). scripts/vm/demo_setup.sh takes the same switch. Run staging's copy of the
script: sudo systemd-run --unit=rl-overnight-staging --uid=racinglines --setenv=TARGET=staging --setenv=MODE=replay
/opt/racinglines-staging/scripts/vm/overnight.sh. TARGET=prod, the default, is unchanged.
Queue. QUEUE names the search queue f1 mode runs (default sweeps/overnight-vm.toml). The script reads the
queue's [search] name (it stops if there is none) and writes the run to data/runs/search/<name>/, with the promoted
queue copied there as queue-promoted.toml; the checkout's queue file is then restored, so the next vm.sh deploy
finds a clean tree. sweeps/defaults-2026-10.toml is the queue for the default-strategies refresh of the F1
Polymarket taker library (owner's go, 2026-10-11): its header has the phases, the job count and the promotion rule,
and the command, run as a transient unit: TARGET=staging MODE=f1 QUEUE=sweeps/defaults-2026-10.toml
scripts/vm/overnight.sh.
| Mode | Writes | What |
|---|---|---|
dry |
nothing | cores, memory, disk; table counts; each sport's tape probe and last-race replay; the F1 golden check (profile A on 2026 at 4k, not saved) |
f1 |
sweep runs, walk-forward | backup; the broad sweep (QUEUE, default sweeps/overnight-vm.toml; output in data/runs/search/<its [search] name>/); search-report; scripts/vm/promote_16k.py appends the top 10 plus profile A and the default settings at 16k; the 16k confirmations; search-report again; with F1_EXTRAS=1 only, the F1 walk-forward and the default F1 sweeps saved (as the backfill's full did; off by default since the 2-vCPU resize) |
search |
sweep runs | backup; the F1 search QUEUE exactly as written (no promotion, no extras); search-report; a data_changes entry naming the backup. For a small confirmation queue, such as the defaults-2026-10 seed check (sweeps/seedcheck-2026-10.toml) |
replay |
links, tape, replay runs | backup; for each of SPORTS (default NASCAR and MotoGP together): if the preflight finds 0 events or 0 Kalshi links with a race, it first fetches and ingests the sport's results 2016-2026, re-syncs Kalshi and (NASCAR) runs nascar link --apply; Kalshi and Polymarket link syncs for 2025 and 2026 (closed included); tape pull and replay (a sport with no tradeable Kalshi market is skipped with a STOP line, the rest go on); the spike check; the replay saves; a read-only settings grid and its top 3 at 16k; then OG.com's read-only buy-all for F1 and NASCAR (PR #88) |
story |
16 sweep runs, the taker's demo backfill | backup; evidence sweeps; a gate that stops on a different pick; the taker's backfill rebuilt (below) |
sports |
replay and story | replay, then story: the resume after the 2026-09-30 failure (F1 already done) |
all |
all three | replay, then story, then f1 (the sports first; F1 has been backtested for days). Each starts only if the one before finished |
Preflight (every mode, first minute). Each sport's event count and its Kalshi links with a race attached. The
first overnight run (2026-09-30) lost its NASCAR phase to this: the VM had 13,599 NASCAR Kalshi links and no NASCAR
results, because nascar ingest and nascar link --apply had only run on the Mac. dry now prints NOT READY for such
a sport; replay loads its results and identifies its links after its backup, and skips it with a STOP line only if
that doesn't fix it. Since #100 a failed load (a fetch or an ingest that errors) is logged as a progress line and the
run goes on: the preflight after it decides whether the sport runs or is skipped, so one sport's data problem never
stops the other sports or the later phases. MotoGP's load ingests the seasons with Kalshi markets (2025-2026) first,
then 2016-2024, whose failure is logged and not fatal.
Every writing mode backs up first and checks the dump's trailer, logs to data/runs/logs/overnight-<mode>-<UTC>.log,
leaves an overnight-<mode>.done or .failed marker, adds a data_changes note naming the backup, and refuses to start
while a racinglines-live-* unit is active. Round 16's book opens Thu 1 Oct 20:30 PDT: the run must start early enough
to end before then.
Progress updates¶
The owner asked (2026-09-30) for clear but not too frequent updates, then for more inside a phase: during the NASCAR
tape pull on 2026-09-30 nothing was logged from 12:59Z to 13:44Z, so nobody could tell working from stuck. The runner
writes one line:
- when each phase starts (the same line closes the phase before, with its duration);
- when each step inside a phase starts (the same line closes the step before, with its duration);
- when each sport starts, and one summary line when it finishes or is skipped;
- after each tape pull and the replay saves, with what they stored, and the grid's top 3;
- every 5 minutes inside a phase (HEARTBEAT_MIN), with the step, its counter and the run log's last line and age;
- at once on any failure or stop (a failure line also carries the step and the run log's last line).
Each line has the UTC time, the mode, the minutes since the start and the failures so far. It also names the phase:
F1 broad sweep, F1 top 10 at 16k, walk-forward, each sport's links and tape, replay saves and settings grid, OG.com
buy-all, and the taker story's sweeps, gate and rebuild. For the F1 searches, the story's evidence sweeps and the
grids, it adds jobs done of total and a rough time left. For a tape pull or a replay it adds the race it is on, of how
many, and a rough time left for the step (scripts/vm/progress.py, reading the race lines the replay prints).
The run sets PYTHONUNBUFFERED=1, so the commands' own per-race and per-job lines reach the run log as they happen.
Before, Python held them in 8 KB blocks when writing to the log, which is likely part of why the 12:59Z-13:44Z gap
looked empty.
The lines, as they look in overnight-progress.log. The check-ins' existing started:, done:, still running:,
STOP:, FAILED at line, backup, preflight, NOT READY and FINISHED lines keep their start; the new parts are
appended after them:
2026-10-01T12:59Z [all, 20 min in, 0 failed] sport 1 of 2: nascar
2026-10-01T12:59Z [all, 20 min in, 0 failed] started: nascar: links, tape, replay
2026-10-01T12:59Z [all, 20 min in, 0 failed] step 1: nascar Kalshi link sync 2025, 2026
2026-10-01T13:03Z [all, 24 min in, 0 failed] step 2: nascar Polymarket link sync 2025, 2026 (step 1 took 4 min)
2026-10-01T13:05Z [all, 26 min in, 0 failed] step 3: nascar Kalshi tape pull and replay 2016-2026 (step 2 took 2 min)
2026-10-01T13:20Z [all, 41 min in, 0 failed] still running: nascar: links, tape, replay, 21 min · step 3: nascar Kalshi tape pull and replay 2016-2026, 15 min · kalshi tape race 57 of 213, about 41 min left · last output 0 min ago: pull kalshi 57/213 2019-14 Pocono: 38 markets, 412 trades, 610 prices
2026-10-01T14:02Z [all, 83 min in, 0 failed] nascar Kalshi: Pulled kalshi: 213 races, 51022 trades, 88410 prices
2026-10-01T15:40Z [all, 181 min in, 0 failed] nascar grid top 3 (edge, floor): 0.1 200; 0.08 200; 0.1 50
2026-10-01T16:30Z [all, 231 min in, 0 failed] sport nascar finished in 211 min · Pulled kalshi: 213 races, 51022 trades, 88410 prices; Pulled polymarket: 36 races, 0 trades, 0 prices; Stored 36 runs, batch replay-20261001T1405Z
The numbers above are made up to show the shape. What to read:
- QUIET: last output N min ago in a heartbeat means the run log has had no new line for QUIET_MIN (30) minutes.
Tape pulls, replays and searches print a line per race or per job, so a QUIET step there is worth a look (tail the
run log); a link sync, an ingest or a single saved sweep can be quiet for a while without being stuck.
- step N: ... (step N-1 took M min) is the quickest way to see where the time went afterwards.
- sport <s> finished|skipped in M min is the per-sport result line; a skipped sport also has a STOP: line above it.
The lines go to data/runs/logs/overnight-progress.log (appended) and data/runs/logs/overnight-status.txt (the latest
line only). Heartbeats go only to those two files, not to the run log, so the run log's age is real output. They are
local files: the job makes no network calls for them. The heartbeat is a background loop that can't fail the run. The
VM can't post to the project, so the Mac reads the files over SSH (the handoff has the command). Knobs, as environment
settings on the unit: HEARTBEAT_MIN (default 5, was 15 then 45; owner rule 2026-09-30) and QUIET_MIN (default 30).
F1: broad sweep, then 16k¶
F1 is the golden baseline, so it goes first and is checked before anything else is read. The sweep numbers on the VM won't match the cloud sweep to the cent, because the VM's database ids differ from the Mac snapshot's (data off GitHub). The pass is profile A's 2026 update P&L within ±$400 of +$1,243 (the measured noise floor for the update taker, PR #85 section 2). If it misses, stop and compare the VM's F1 market links with the Mac's before trusting anything else.
The queue is 106 jobs at 4,000 simulations, Polymarket only, 2026 the target and 2025 held out. The size is 48 settings × 2 seasons, plus profile A's 2 extra seeds × 2 seasons, plus 6 default-settings runs (3 seeds × 2 seasons). Jobs run in this order:
| Group | Settings | What it asks |
|---|---|---|
| golden | 1 (3 seeds) | Does A reproduce on the VM, and what is the noise floor on this data? |
| model | 20 | A with one model setting changed: half-life 60/90/180/240 days, reset weight 0/0.1/0.5, driver shrinkage 1/6, team shrinkage 1/4, slope shrinkage 3/12, teammate finish correlation 0.5/1.5, practice prior off, track features off, teammate noise off, plus the tail and rookie variants on A |
| taker | 12 | A with one taker setting changed: min edge 0.07/0.09/0.12/0.15, head-to-head edge 0.03/0.08/none, volume floor $25/$100/$400, stage-aware stop points (after FP2 and qualifying; after qualifying only) |
| cross | 15 | model (gridq+pretrain, gbm, A's) × half-life 90/180 × edge 0.08/0.12, and A's taker settings on baseline, gbm, gridq+pretrain |
What is left out on purpose: a volume floor of 0 (untradeable, PR #85), stake changes (sizing, not edge), Kalshi takers
(invalid until bid/ask), and PR #81's switches (inert on history or inside the noise). The queue stops starting jobs
after 3 hours, so a slow night cuts the cross group first. promote_16k.py then takes the 10 best distinct settings
from candidates.toml, keeps only Polymarket with a volume floor of at least $25, and queues each for both seasons at
16,000 simulations with profile A and the default settings: at most 24 jobs.
Decision rule for the morning: a setting replaces profile A only if it is ahead in both seasons at 16k by more than
the noise floor measured tonight. Otherwise A stays, and the top 10 feed the Pro list as PR #85 set up. Either way the
change gets a decision-log entry in docs/f1-roadmap.md before it is treated as final.
NASCAR: tape, replay, grid¶
The replay settings in sports/nascar.toml are a priori, none tuned. The grid is 16 read-only runs on Kalshi:
min edge 0.05/0.08/0.10/0.15 × volume floor $50/$200 × seasons 2026 and 2025. It is ranked by the worse season's update
P&L after Kalshi's taker fee (scripts/vm/replay_grid.py rank). The top 3 re-run at 16,000 simulations (the NASCAR
model's default is 2,000): 6 more runs. The grid stops starting runs after 3 hours (GRID_HOURS).
Read NASCAR's P&L only after the spike check. Kalshi's stored price is the candle's last trade, and that is what
ruled out Kalshi's F1 taker replay (1.67% of points more than 15 points off their rolling median, against 0.06% on
Polymarket). replay_grid.py spikes nascar kalshi prints the same share for NASCAR, and the share of prices at exactly 0.50 (the
dead-book midpoint PR #89 stops storing; a pull before #89 may have left some), with a verdict. It runs after tonight's
pull, so on #89's prices:
- clean: the P&L can be read;
- in between: read the per-race table, not the total;
- spiky: the P&L is indicative only, and NASCAR waits for the bid/ask re-pull (N4's method) before any Pro pick
uses it.
Demo taker walk-forward story¶
The owner asked (2026-09-30) for the demo taker's history to follow the walk-forward story of the taker re-sweep
(PR #85, section 6) instead of running profile A with hindsight from the first race of 2025. The decision rule and
decision points are the demo maker's (pipelines/story.py), on the round-1 pool fixed before any result, so each phase
is out of sample for the decision that picked it:
| Phase | Weekends | Profile | Why it was picked | Sweep P&L then (out of sample) | A over the same weekends (in sample) |
|---|---|---|---|---|---|
| TW1 | 2025 rounds 1-8 | defaults: baseline model, update | no evidence yet | −$129 | part of A's 2025 +$1,363 |
| TW2 | 2025 rounds 9-24 | gridq+pretrain+reset, min edge 0.10, update | P&L leader after round 8 (+$327) | +$870 | part of A's 2025 +$1,363 |
| TW2 | 2026 | the same (kept after round 16 and before 2026) | still the leader, then most consistent within $400 | +$1,099 | A's 2026 +$1,243 |
The sweep P&L is at the default stake (4k). The demo taker account follows about a third of its picks
(profiles.DEMO_FOLLOW), so its paper P&L is roughly a third of those figures. The app labels every backfilled row
as a backtest replay, and the rows stay labelled demo. The story never picks a blend (section 6), so the blend stays
a Pro option only.
How it runs: the story's code is PR #87 (the taker's HISTORY becomes TW1 then TW2, and the Strategy page
shows the taker's decisions with their evidence). It must be merged and deployed before this step; no migration.
story mode runs after the NASCAR part, in this order, and leaves an overnight-story.done or .failed marker:
- Backup:
data/backups/db/racinglines-before-demo-taker-story-<UTC>.sql.gz, trailer checked. - Evidence sweeps:
racinglines strategy search f1 sweeps/demo-taker-story.toml(2025, the 16 settings of the story's pool, 2 at a time, about 20 to 40 minutes), logged todata/runs/search/demo-taker-story.log. It must end withsearch: finished (16 done). They are saved runs (model_runs), so no bucket stage runs at the same time. - Gate, an explicit stop:
scripts/vm/story_gate.pyrunsstory.decisions(conn, taker=True)and must see the defaults before round 1, thenupdate · gridq+pretrain+reset · min_edge=0.1after 2025 round 8, kept twice, with 32 candidates at the last decision. Anything else, or 0 candidates, stops the mode with nothing reset. - Rebuild:
racinglines strategy demo-history f1 --reset --user taker. Always with--user taker. Never a bare--reset, and neverf1 demo-history --venue polymarket --resetwithout it: that would also delete the demo maker's Polymarket record, which may not rebuild on the VM. - Log: a
data_changesrow naming the backup; the thread adds thedocs/data-changes.mdline in the morning.
Order against the other work¶
- Bucket stages 1 and 2 first (Mac, minutes; handoff
2026-09-30-data-off-github-stages-1-2.md). Their checks must pass before anything writes to the VM's database.vm.sh deployrunsalembic upgrade head, so the deploy counts as a write too. After that, no bucket stage runs until the overnight run's marker is in: the bucket and the VM database never change at the same time. - The Mac spot check of the new pieces: the promotion, the grid ranking and the spike check (owner rule: a Mac stop before a VM run of new backtest logic).
- Resize the VM for the night. It is an e2-small: 2 shared vCPUs (about 1 sustained) and 2 GB of memory, which
also runs Postgres, the web app and MCP. Parallel sweeps would run out of memory, and on one core the run
would take well past morning. The plan asked for an e2-standard-4, but us-west1-b had no capacity for it
(
ZONE_RESOURCE_POOL_EXHAUSTED, 2026-09-30), so tonight's VM is an n2-standard-2 (2 dedicated vCPUs, 8 GB). The F1 queues run 2 at a time (parallel = 2): 8 GB carries two sweeps with the history cache and Postgres, a third would only queue for the same 2 vCPUs, and every run is niced so the web app and MCP stay responsive. The resize needs a stop and a start: racinglines.bet is down for a minute or two, the Cloudflare tunnel reconnects on its own (it's outbound only), and the services start on boot. Resize back to the original e2-small the same way once the run's marker is in. If the owner would rather not resize, runMODE=f1on the e2-small withparallel = 1andhours = 3: then only the golden and model groups finish. - Merge in this order, then deploy main (
vm.sh deploy): #89, #90 (the candle fix), #88 (OG.com buy-all), #87 (the taker story), then this PR. Both Kalshi fixes must be deployed before any tape pull or replay save on the VM: - PR #89 rejects the dead-book 0.50 midpoint in the live quote path (
kalshi/sync.py _quote,exchange_driver.quote) and keeps MotoGP's UUID rider ids. - PR #90, the candle fix. The tape pull stores prices through
kalshi/sync.py history_rows, which #89 does not touch. It still averages an empty book's bid of 0 and ask of 1 into a 0.50 price (checked on main:history_rowsreturns 0.5 for such a candle). The fix is to skip the midpoint when a side is empty, the same rule as #89.replaymode checks for it and stops before any pull if the checkout still stores 0.50. - PR #88, OG.com replay prices.
racinglines markets --exchange og --sport <sport> buy-all, read-only: one YES and one NO of every OG.com market at the first stored price, held, settled or marked.replaymode runs it for F1 and NASCAR after the replays (OG.com lists no MotoGP) and writesdata/runs/replay-grid/og/<sport>.csv. Its Mac spot check is step 4 ofhandoffs/2026-09-30-buy-all-spot-check.md. OG.com's recorded prices are on the VM, so aNO PRICEon the Mac is expected; the VM run is where they count. - Mac cleanup first: run section 1e of the backfill handoff on the Mac (a read-only count of 0.50 prices, then back up, delete NASCAR's and MotoGP's Kalshi price rows and pull them again), so the spot-check numbers are clean.
- The VM's older candles: the VM's 2026-09-29 sync stored hourly Kalshi prices for 2026-08-29 to 09-29 through
the same
history_rows. A re-pull after the fix writes no row for an empty hour, so it doesn't overwrite an old 0.50 row. The dry run prints each sport's share at exactly 0.50 (read-only). If it's well above zero, those rows get the same cleanup as the Mac, as its own step with a backup and the owner's word.
Then the dry run. Go on only if the golden check passes, the counts show nascar_cup races for 2025 and 2026, and the Kalshi probe returns trades and prices.
MotoGP joins tonight only if the counts also show motogp_wc races and its probe is above 0; otherwise NASCAR alone.
5. MODE=all (F1, then NASCAR and MotoGP, then the demo taker story), started early in the evening, well before a
live event.
6. Morning: markers, log greps, the two reports (data/runs/search/overnight-vm/report.md,
data/runs/replay-grid/<sport>/grid.md) and the spike lines pasted back to the thread, which writes the report,
the decision-log entry and the docs/data-changes.md line. Resize back.
7. After the run's marker is in: the NASCAR champion forecast (PR #93, already on the VM, no redeploy). Read-only:
nascar fetch only downloads the points feed to data/raw/nascar/, and nascar season writes nothing to the
database, so no backup. It fills the NASCAR × OG.com cell at the priced level:
racinglines data fetch nascar --years 2026 --feeds points-feed, then
RACINGLINES_NASCAR_SEASON=1 racinglines model season nascar --quotes --csv data/runs/nascar-season-2026.csv.
8. Later, each with its own go: M3 (OG.com venue switch), M4 if MotoGP didn't join tonight (MODE=replay SPORTS=motogp), N1 to N5, and bucket stages 3 onward.
Expected time (estimates from the cloud sweep's rate, not measured on the VM)¶
| Piece | Runs | Time on the n2-standard-2 (2 vCPU) |
|---|---|---|
| Dry run, including the golden check | 1 sweep | 30 to 80 min |
| F1 broad | 106 jobs, 2 at a time | 2 to 4 h, capped at 3 h (+20 min grace): about 75 to 100 jobs finish, the cross group is cut first |
| F1 16k | up to 24 jobs at 4× the cost | 1 to 3 h |
| F1 walk-forward and default sweeps | 5 | about 2 h, off by default (F1_EXTRAS=1) |
| NASCAR and MotoGP links and tape | several thousand Kalshi markets at 4 requests/s | 1 to 3 h (network-bound, unchanged by the resize) |
| Replay saves | about 360 runs per sport | 1 to 2 h |
| Settings grid and 16k | 16 + 6 per sport | capped at 3 h, plus the 16k runs |
| Demo taker story | 16 evidence sweeps, then about 38 weekends replayed | 1 to 3 h |
The total is about 8 to 16 hours. Started around 01:00 PDT, F1 is done by morning and the replay and story phases finish by early afternoon, well before round 16's book opens (Thu 1 Oct 20:30 PDT). The progress lines' "about N min left" comes from each phase's own measured rate, so it adjusts to the smaller VM by itself.
If it has to be shorter, cut in this order: (1) the F1 extras, already off (saves about 2 h; run them any later
day with MODE=f1 F1_EXTRAS=1, which skips the search jobs already finished); (2) GRID_HOURS=1 (whole hours; saves up to
2 h per sport); (3) SPORTS=nascar (saves MotoGP's links, tape, saves and grid, about 2 to 4 h, and leaves its row
for a later MODE=replay SPORTS=motogp). The 16k top 10 stays: it feeds the Pro list.
Rollback¶
- Demo taker story: restore
racinglines-before-demo-taker-story-…(the step 1 dump). - Replay saves:
racinglines strategy replay nascar --undo BATCH, with the batch ids from the log (grep "batch replay-"). - Sweep and walk-forward runs: ordinary saved runs; they can stay.
- Links and tape: additive upserts of exchange data, the kind the recorder writes; harmless to keep.
- Last resort: restore
racinglines-before-overnight-f1-…or…-replay-…. A restore also drops anything written since, so ask in the thread first. - The resize: stop, set the machine type back to e2-small (from tonight's n2-standard-2), start.
What this plan doesn't cover¶
- The web app's all-sport P&L demo histories (the owner asked for them after the backfill; they need a separate go).
- Kalshi's F1 taker, OG.com backtests and NASCAR's OG.com cell (N3 to N5).
- Whether the VM's database already has NASCAR's 2025 results: the dry run's counts answer it. If it doesn't,
nascar ingestcomes first, with its own backup and the owner's word.