d604bb14ff
- 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>
114 lines
4.4 KiB
TypeScript
114 lines
4.4 KiB
TypeScript
import { readFileSync, existsSync } from 'node:fs';
|
||
import { eloDelta, importanceWeight } from '../../src/lib/model/elo';
|
||
import { predictMatch, type RatingsModel } from '../../src/lib/model/predict';
|
||
import { hostAdvantage } from '../../src/lib/model/hosts';
|
||
import { buildSimulator } from '../../src/lib/model/monteCarlo';
|
||
import type { Fixture, MatchPrediction, ModelSnapshot, OddsHistoryPoint } from '../../src/lib/types';
|
||
|
||
const ITERATIONS = Number(process.env.SIM_ITER) || 20_000;
|
||
const WC_K = importanceWeight('FIFA World Cup');
|
||
|
||
function loadRatings(staticDir: string): RatingsModel {
|
||
const candidates = [
|
||
`${staticDir}/data/ratings.json`,
|
||
`${process.cwd()}/public/data/ratings.json`,
|
||
`${process.cwd()}/dist/data/ratings.json`,
|
||
];
|
||
for (const p of candidates) {
|
||
if (existsSync(p)) return JSON.parse(readFileSync(p, 'utf8')) as RatingsModel;
|
||
}
|
||
throw new Error(`ratings.json not found. Run "npm run data:ratings". Looked in: ${candidates.join(', ')}`);
|
||
}
|
||
|
||
/** Owns the predictive model: live Elo re-rating, per-match probabilities, and
|
||
* the Monte Carlo tournament simulation. Recomputes only when results change. */
|
||
export class ModelEngine {
|
||
private readonly base: RatingsModel;
|
||
private history: OddsHistoryPoint[] = [];
|
||
private finishedKey = ' |