FIS alpine skiing¶
The newest sport (2026-10-22): FIS Alpine World Cup, World Championships and Olympic races since the 2018/19 season,
priced per gender and discipline by the one engine. Built in one night as the reference "new sport, new model,
tuned model, fair prices" run; the working notes and the sweep reports are local (reports/2026-10-23-alpine-*),
the code is on track/models.
Data¶
- Source: the FIS results database (
www.fis-ski.com/DB), server-rendered HTML, probed read-only from the owner's Mac first (Data). Terms: clause 6 forbids scraping and commercial reproduction; the owner cleared single-user research with no reproduction (2026-10-22), to be checked again before any release. The pull runs on the alpine VM at one request per 2 s; raw pages never enter git. - Fetcher and ingest:
racinglines data fetch fis_alpine,racinglines data ingest fis_alpine --backup FILE(racinglines/sources/fis_alpine/). One database event per FIS race (sourcefis_db, key = raceid); categories are gender x discipline (MDH, MSG, MGS, MSL, WDH, WSG, WGS, WSL); roundsrace(official classification),run1,run2(per run; run2 is DNS for anyone who never started it),practice(downhill training), with one-run races storing their run asrun1too. Athletes are keyed by FIS competitor id (identifier schemefis, plusfiscode). Team, parallel and combined pages are refused. 2018/19-2025/26: 1,043 events, 844 individual races, 1,482 athletes.
Model¶
Three cores of the one engine are compared on the same data (sports/fis_alpine.toml):
| Arm | What it is | How it is selected |
|---|---|---|
| global | the results core: probit strengths from finishing positions | [model] inputs with core = "results" (the default) |
| rank_logit | the Plackett-Luce / Davidson-Luce challenger on the results core | --model rank_logit |
| timed_runs | the run-times core on run times: run 1 as qualifier, run 2 as a final ranked on the summed time, one-run races as a single deciding run | --model timed_runs (racinglines/models/challengers.py) |
[model] history = "category" fits one history per category (both results-core arms). The run-times core is bound
per sport: [rounds] map renames the data rounds, season = "schema" uses FIS season codes, cumulative_final,
starters_only and result_round describe alpine's format. Settings are tuned by the free-running optimizer
(scripts/research/alpine_walk.py: compass search with hammer proposals, dynamically sized steps, a simulation
ladder and a Monte Carlo noise gate), scoring seasons 2021/22-2023/24; 2024/25 and 2025/26 are held out and scored
once, for the finalists only.
Decision log¶
| Date | Decision | Why | Evidence |
|---|---|---|---|
| 2026-10-22 | Categories = gender x discipline; the engine fits per category | A skier's strength is discipline-specific (owner: "separate all events"); both cores pooled categories before [model] history = "category" |
commit bfef701d |
| 2026-10-22 | Held-out = seasons 2025 and 2026, scored once for the finalists; sweeps score 2022-2024 | Pre-registered before any sweep | notes |
| 2026-10-23 | Simulation rungs: explore at 1k, confirm at 10k; 100 unusable, 100k adds nothing | Ladder on the round-3 grid: seed-pair noise 0.00024 at 1k, rank correlation 0.995 vs 10k; 10k matches 100k to 0.00006 | running report, img/ladder.png |
| 2026-10-23 | rank_logit's fitted temperature and place_decay rejected; its sharpening knob is the athlete prior width | Round 2: every fitted-temperature job worse (mean podium LL 0.164 vs 0.154) | reports/2026-10-23-alpine-ab-round2 |
| 2026-10-23 | The timed-runs core joins as a third arm, bound per sport | Alpine results are times; rank models discard margins (owner: "yes to timed runs") | commit 3773102e |
| 2026-10-23 | Model-only sizing on slips for a sport with no exchange quotes (book render --require model) |
No venue lists alpine World Cup races | commit 56648d15 |
| 2026-10-23 | Default = the results core (global) with half-life 174.2 d, prior weight 1.1, few-starts spread 0.03, noise 0.5, calibration power 1, DNF model off. Golden held-out 2025+2026 (160 races, 10k sims, seed 1, scored once): podium log loss 0.1385 vs rank_logit 0.1430 (paired -0.0045 [-0.0066, -0.0024]) vs timed runs 0.1537 (-0.0152 [-0.0197, -0.0105]); uniform 0.2113. Pre-registered rule met (interval excludes zero). Tuned beat runner-up in every arm out of sample (global -0.0010 [-0.0018, -0.0002]). rank_logit (hl 240, sd 7.465, top-k 10, temp 0.97, place decay 4) and timed runs (hl 160, prior n 0.5, all seasons, eps df 2.2) stay as dissent on slips, never as extra bets. |
reports/2026-10-23-alpine-e2e/GOLDEN.md, CARD-final.md, MORNING.md | walker v3 (f8f290cc), confirm-final/, holdout/ |
Where it runs (2026-10-24)¶
The sport is audience = "admin": admins see it, nobody else, which is the experimental dashboard. Its data lives in
racinglines_staging on the VM (ingested by scripts/vm/alpine_staging.sh from the saved FIS pages; the dedicated
build VM rl-alpine holds the raw tree and the research runs) and not in production. [model.defaults] in
sports/fis_alpine.toml are the golden settings, so every price the app shows uses them.
Known limitations¶
- Settings are tuned on a fixed race set: a configuration that cannot price some races (for instance the timed-runs
core's
train_scope = season, which prices from the target season only) is charged the uniform baseline for them, and that scope is not offered to the optimizer. No arm forgets prior seasons; recency is the half-life's job. - Fields in backtests are the actual starters; live pricing needs the start-list PDFs (not fetched).
- No FIS-points baseline yet: the race pages carry post-race points, the pre-race points lists were not fetched.
- The two-run total is simulated as exp(t1) + exp(t2) with no run-specific course effect.
- Only Olympic alpine markets have been listed on Kalshi and Polymarket; the private venue prices the World Cup.