Phase 2: predictive model — Elo + Dixon-Coles + Monte Carlo
- buildRatings.ts: walks 49k historical results → World-Football-Elo per team, data-calibrated goals model (goals-per-Elo slope, mean goals) + MLE-fit Dixon-Coles rho. Top: Spain/Argentina/France (the real 2026 favourites) - src/lib/model: elo, poisson/dixon-coles, predict, host-advantage, monteCarlo (full 48-team sim — group sampling, best-third bipartite allocation, knockout advance probs). 20 vitest cases incl. exact per-round count invariants - Server ModelEngine: live Elo re-rating after each result, per-match W/D/L, 20k-sim odds, odds-over-time history; broadcast on finished-result changes - Client: championship board, heat-shaded odds table, lazy-loaded title-race chart (Recharts split to its own chunk), match-prediction bars, bracket advance overlay - Verified: odds render, chart populates as injected results re-rate teams live Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+54
-2
@@ -69,9 +69,61 @@ export interface Snapshot {
|
||||
tables: Record<string, StandingRow[]>;
|
||||
}
|
||||
|
||||
// ---- model / predictions ----
|
||||
|
||||
/** Per-team tournament odds from the Monte Carlo simulation. */
|
||||
export interface TeamOdds {
|
||||
team: string;
|
||||
winGroup: number;
|
||||
qualify: number;
|
||||
reachR16: number;
|
||||
reachQF: number;
|
||||
reachSF: number;
|
||||
reachFinal: number;
|
||||
champion: number;
|
||||
}
|
||||
|
||||
export interface MatchProbs {
|
||||
home: number;
|
||||
draw: number;
|
||||
away: number;
|
||||
}
|
||||
|
||||
/** A single per-match prediction (group + resolved knockout games). */
|
||||
export interface MatchPrediction {
|
||||
num: number;
|
||||
home: string;
|
||||
away: string;
|
||||
probs: MatchProbs;
|
||||
lambdaHome: number;
|
||||
lambdaAway: number;
|
||||
topScore: { home: number; away: number; p: number };
|
||||
}
|
||||
|
||||
/** A point on the championship-odds-over-time chart. */
|
||||
export interface OddsHistoryPoint {
|
||||
t: string;
|
||||
label: string;
|
||||
top: { team: string; champion: number }[];
|
||||
}
|
||||
|
||||
export interface ModelSnapshot {
|
||||
generatedAt: string;
|
||||
/** Date the underlying ratings reflect (advances as results re-rate teams). */
|
||||
asOf: string;
|
||||
iterations: number;
|
||||
matches: MatchPrediction[];
|
||||
odds: TeamOdds[];
|
||||
history: OddsHistoryPoint[];
|
||||
}
|
||||
|
||||
// ---- WebSocket protocol ----
|
||||
// The snapshot is small (~104 fixtures), so the server simply re-pushes the full
|
||||
// snapshot on every change — no diffing, no client-side merge races.
|
||||
// snapshot on every change — no diffing, no client-side merge races. Predictions
|
||||
// are heavier to compute, so they ride a separate message sent only when the set
|
||||
// of finished results changes.
|
||||
|
||||
/** Server → client. */
|
||||
export type ServerMessage = { t: 'snapshot'; snapshot: Snapshot };
|
||||
export type ServerMessage =
|
||||
| { t: 'snapshot'; snapshot: Snapshot }
|
||||
| { t: 'predictions'; model: ModelSnapshot };
|
||||
|
||||
Reference in New Issue
Block a user