v3 Phase A (urgent): bookmaker odds capture — the Model-vs-Market benchmark

- docs/V3-GIGA-PLAN.md: the v3 plan (data lake to 7+GB, GIGA ensemble, scoreboard,
  UI clarity) with the honest reframe: perfect accuracy = perfect calibration;
  'beat the betting sites' = a live public head-to-head, scored with proper rules.
- src/lib/odds.ts: American-ml → decimal, de-vig (normalize away the margin),
  overround; 3 vitest cases incl. tonight's real opening lines.
- ESPN core odds adapter (server/src/ingest/espnOdds.ts): captures DraftKings
  1X2 moneylines + O/U + spread per fixture; follows $ref pointer items.
- odds_history table (insert-if-changed → clean line-movement history);
  scheduler odds sweep every 3h over a 14-day horizon, first sweep at boot;
  /api/odds + /api/odds/:num. Odds are a benchmark ONLY — never a model input.
- Boot speed-up: fixture↔event mapping skips dates already fully mapped.
- Verified: 54 fixtures captured with real moneylines locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 17:03:15 +02:00
parent 51dfe00216
commit d887664fce
8 changed files with 295 additions and 5 deletions
+8 -1
View File
@@ -9,7 +9,7 @@ import { TournamentState } from './tournament';
import { startScheduler } from './ingest/scheduler';
import { ModelEngine } from './model';
import { buildPreview, buildTeamProfile } from './preview';
import { db, healthAll } from './db/db';
import { db, healthAll, oddsFor, latestOddsAll } from './db/db';
import { canonicalTeam } from '../../src/lib/teams';
import type { MatchStatus, ServerMessage } from '../../src/lib/types';
@@ -57,6 +57,13 @@ export function buildServer() {
app.get('/api/snapshot', async () => state.snapshot());
app.get('/api/predictions', async () => model.current());
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/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' });
return { history: oddsFor(num) };
});
app.get('/api/preview/:num', async (req, reply) => {
const num = Number((req.params as { num: string }).num);
const preview = Number.isFinite(num) ? buildPreview(num, state, model) : null;