Skip to content

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 (source fis_db, key = raceid); categories are gender x discipline (MDH, MSG, MGS, MSL, WDH, WSG, WGS, WSL); rounds race (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 as run1 too. Athletes are keyed by FIS competitor id (identifier scheme fis, plus fiscode). 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.