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:
@@ -85,6 +85,14 @@ CREATE TABLE IF NOT EXISTS odds_history (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_odds_fixture ON odds_history(fixture_num, captured_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prediction_snapshots (
|
||||
fixture_num INTEGER PRIMARY KEY,
|
||||
taken_at INTEGER NOT NULL,
|
||||
late INTEGER NOT NULL DEFAULT 0,
|
||||
model_json TEXT NOT NULL,
|
||||
market_json TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ingest_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT NOT NULL,
|
||||
@@ -229,6 +237,31 @@ export function latestOddsAll(): OddsRow[] {
|
||||
.all() as unknown as OddsRow[];
|
||||
}
|
||||
|
||||
// ---- frozen pre-kickoff forecasts (Model-vs-Market scoreboard) ----
|
||||
export interface SnapshotRow {
|
||||
fixture_num: number;
|
||||
taken_at: number;
|
||||
late: number;
|
||||
model_json: string;
|
||||
market_json: string | null;
|
||||
}
|
||||
|
||||
export function hasSnapshot(fixtureNum: number): boolean {
|
||||
return !!db().prepare('SELECT 1 FROM prediction_snapshots WHERE fixture_num = ?').get(fixtureNum);
|
||||
}
|
||||
|
||||
export function saveSnapshot(fixtureNum: number, late: boolean, model: unknown, market: unknown | null): void {
|
||||
db()
|
||||
.prepare('INSERT OR IGNORE INTO prediction_snapshots (fixture_num, taken_at, late, model_json, market_json) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(fixtureNum, Date.now(), late ? 1 : 0, JSON.stringify(model), market ? JSON.stringify(market) : null);
|
||||
}
|
||||
|
||||
export function allSnapshots(): SnapshotRow[] {
|
||||
return db()
|
||||
.prepare('SELECT fixture_num, taken_at, late, model_json, market_json FROM prediction_snapshots ORDER BY fixture_num')
|
||||
.all() as unknown as SnapshotRow[];
|
||||
}
|
||||
|
||||
// ---- per-fixture enrichment blob ----
|
||||
export function setMatchExt(fixtureNum: number, json: unknown): void {
|
||||
db().prepare('INSERT OR REPLACE INTO match_ext (fixture_num, json, updated_at) VALUES (?, ?, ?)')
|
||||
|
||||
Reference in New Issue
Block a user