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:
@@ -72,6 +72,19 @@ CREATE TABLE IF NOT EXISTS goalscorers (
|
||||
CREATE INDEX IF NOT EXISTS idx_goalscorers_scorer ON goalscorers(scorer);
|
||||
CREATE INDEX IF NOT EXISTS idx_goalscorers_team ON goalscorers(team);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS odds_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
fixture_num INTEGER NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
captured_at INTEGER NOT NULL,
|
||||
home_ml REAL,
|
||||
draw_ml REAL,
|
||||
away_ml REAL,
|
||||
over_under REAL,
|
||||
spread REAL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_odds_fixture ON odds_history(fixture_num, captured_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ingest_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
source TEXT NOT NULL,
|
||||
@@ -173,6 +186,49 @@ export function getSourceMap(fixtureNum: number, source: string): string | null
|
||||
return row?.event_id ?? null;
|
||||
}
|
||||
|
||||
// ---- bookmaker odds history (benchmark only, never a model input) ----
|
||||
export interface OddsRow {
|
||||
fixture_num: number;
|
||||
provider: string;
|
||||
captured_at: number;
|
||||
home_ml: number | null;
|
||||
draw_ml: number | null;
|
||||
away_ml: number | null;
|
||||
over_under: number | null;
|
||||
spread: number | null;
|
||||
}
|
||||
|
||||
/** Insert a snapshot only if the lines moved since the last capture. */
|
||||
export function recordOdds(o: Omit<OddsRow, 'captured_at'>): boolean {
|
||||
const last = db()
|
||||
.prepare('SELECT home_ml, draw_ml, away_ml, over_under, spread FROM odds_history WHERE fixture_num = ? AND provider = ? ORDER BY captured_at DESC LIMIT 1')
|
||||
.get(o.fixture_num, o.provider) as Omit<OddsRow, 'fixture_num' | 'provider' | 'captured_at'> | undefined;
|
||||
if (
|
||||
last &&
|
||||
last.home_ml === o.home_ml && last.draw_ml === o.draw_ml && last.away_ml === o.away_ml &&
|
||||
last.over_under === o.over_under && last.spread === o.spread
|
||||
) return false;
|
||||
db()
|
||||
.prepare('INSERT INTO odds_history (fixture_num, provider, captured_at, home_ml, draw_ml, away_ml, over_under, spread) VALUES (?, ?, ?, ?, ?, ?, ?, ?)')
|
||||
.run(o.fixture_num, o.provider, Date.now(), o.home_ml, o.draw_ml, o.away_ml, o.over_under, o.spread);
|
||||
return true;
|
||||
}
|
||||
|
||||
export function oddsFor(fixtureNum: number): OddsRow[] {
|
||||
return db()
|
||||
.prepare('SELECT fixture_num, provider, captured_at, home_ml, draw_ml, away_ml, over_under, spread FROM odds_history WHERE fixture_num = ? ORDER BY captured_at')
|
||||
.all(fixtureNum) as unknown as OddsRow[];
|
||||
}
|
||||
|
||||
export function latestOddsAll(): OddsRow[] {
|
||||
return db()
|
||||
.prepare(`SELECT o.* FROM odds_history o
|
||||
JOIN (SELECT fixture_num, provider, MAX(captured_at) m FROM odds_history GROUP BY fixture_num, provider) t
|
||||
ON o.fixture_num = t.fixture_num AND o.provider = t.provider AND o.captured_at = t.m
|
||||
ORDER BY o.fixture_num`)
|
||||
.all() as unknown as OddsRow[];
|
||||
}
|
||||
|
||||
// ---- 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