v3: Model-vs-Market scoreboard — frozen forecasts, fair scoring, live page
- prediction_snapshots: model + de-vigged market frozen ≤15min pre-kickoff (or at first sight of a live match, flagged 'late' and excluded from totals). - src/lib/model/scoring.ts: ONE shared RPS/Brier/log-loss implementation for the backtest and the live scoreboard. - /api/scoreboard + /scoreboard page: per-match model-vs-bookmaker bars, RPS for both after FT, 'closer' badges, running head-to-head with honest rules printed (small-sample caveat included). Nav: 'vs Market'. - Methodology page now shows the three-way split + ensemble bake-off variants. - Verified end-to-end with a simulated match: snapshot froze model 80.7% and DraftKings 67.0% (de-vig correct), both scored at FT, late-flag honored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+7
-1
@@ -9,6 +9,7 @@ import { TournamentState } from './tournament';
|
||||
import { startScheduler } from './ingest/scheduler';
|
||||
import { ModelEngine } from './model';
|
||||
import { buildPreview, buildTeamProfile } from './preview';
|
||||
import { buildScoreboard, snapshotCheck } from './scoreboard';
|
||||
import { db, healthAll, oddsFor, latestOddsAll } from './db/db';
|
||||
import { canonicalTeam } from '../../src/lib/teams';
|
||||
import type { MatchStatus, ServerMessage } from '../../src/lib/types';
|
||||
@@ -59,6 +60,7 @@ export function buildServer() {
|
||||
app.get('/api/sources/health', async () => ({ sources: healthAll() }));
|
||||
// Bookmaker odds (benchmark for the Model-vs-Market scoreboard; not a model input)
|
||||
app.get('/api/odds', async () => ({ odds: latestOddsAll() }));
|
||||
app.get('/api/scoreboard', async () => buildScoreboard(state));
|
||||
app.get('/api/odds/:num', async (req, reply) => {
|
||||
const num = Number((req.params as { num: string }).num);
|
||||
if (!Number.isFinite(num)) return reply.code(400).send({ error: 'bad num' });
|
||||
@@ -93,6 +95,7 @@ export function buildServer() {
|
||||
'football-data',
|
||||
);
|
||||
if (!f) return reply.code(404).send({ error: 'no such fixture' });
|
||||
snapshotCheck(state, model);
|
||||
broadcast();
|
||||
return { ok: true, fixture: f };
|
||||
});
|
||||
@@ -125,7 +128,10 @@ export function buildServer() {
|
||||
});
|
||||
|
||||
// ---- ingestion scheduler (ESPN primary; football-data / SofaScore fallback) ----
|
||||
const stopIngest = startScheduler(state, () => broadcast());
|
||||
const stopIngest = startScheduler(state, () => broadcast(), () => {
|
||||
const taken = snapshotCheck(state, model);
|
||||
if (taken) console.log(`[scoreboard] froze ${taken} pre-kickoff forecast(s)`);
|
||||
});
|
||||
app.addHook('onClose', async () => stopIngest());
|
||||
|
||||
// ---- static SPA (prod) with history fallback ----
|
||||
|
||||
Reference in New Issue
Block a user