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:
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { americanToDecimal, deVig, overround } from './odds';
|
||||
|
||||
describe('americanToDecimal', () => {
|
||||
it('converts favourites and underdogs', () => {
|
||||
expect(americanToDecimal(-235)).toBeCloseTo(1.4255, 3);
|
||||
expect(americanToDecimal(340)).toBeCloseTo(4.4, 3);
|
||||
expect(americanToDecimal(100)).toBeCloseTo(2.0, 6);
|
||||
expect(americanToDecimal(-100)).toBeCloseTo(2.0, 6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deVig', () => {
|
||||
it('produces probabilities that sum to 1 with the margin removed', () => {
|
||||
// tonight's real opening-match lines: Mexico -235 / draw +340 / SA +750
|
||||
const p = deVig(-235, 340, 750)!;
|
||||
expect(p.home + p.draw + p.away).toBeCloseTo(1, 9);
|
||||
expect(p.home).toBeGreaterThan(0.6); // heavy favourite
|
||||
expect(p.away).toBeLessThan(0.15);
|
||||
// raw implied sum exceeds 1 (the vig)
|
||||
expect(overround(-235, 340, 750)).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
it('returns null on missing lines', () => {
|
||||
expect(deVig(null, 340, 750)).toBeNull();
|
||||
expect(deVig(-235, 0, 750)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
// Bookmaker-odds math, shared by the server (capture) and the client
|
||||
// (Model-vs-Market scoreboard). Odds are a BENCHMARK only — never a model input.
|
||||
|
||||
import type { MatchProbs } from './types';
|
||||
|
||||
/** American moneyline → decimal odds (e.g. -235 → 1.4255, +340 → 4.40). */
|
||||
export function americanToDecimal(ml: number): number {
|
||||
if (!Number.isFinite(ml) || ml === 0) return NaN;
|
||||
return ml < 0 ? 1 + 100 / Math.abs(ml) : 1 + ml / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* De-vig a 1X2 set of American moneylines into fair probabilities: convert to
|
||||
* implied probabilities (1/decimal) and normalize away the bookmaker margin.
|
||||
* Returns null if any line is missing/invalid.
|
||||
*/
|
||||
export function deVig(homeMl: number | null, drawMl: number | null, awayMl: number | null): MatchProbs | null {
|
||||
if (homeMl == null || drawMl == null || awayMl == null) return null;
|
||||
const dh = americanToDecimal(homeMl);
|
||||
const dd = americanToDecimal(drawMl);
|
||||
const da = americanToDecimal(awayMl);
|
||||
if (!Number.isFinite(dh) || !Number.isFinite(dd) || !Number.isFinite(da)) return null;
|
||||
const ih = 1 / dh, id = 1 / dd, ia = 1 / da;
|
||||
const total = ih + id + ia;
|
||||
if (total <= 0) return null;
|
||||
return { home: ih / total, draw: id / total, away: ia / total };
|
||||
}
|
||||
|
||||
/** Bookmaker overround (margin) for display: e.g. 1.052 → "5.2%". */
|
||||
export function overround(homeMl: number, drawMl: number, awayMl: number): number {
|
||||
return 1 / americanToDecimal(homeMl) + 1 / americanToDecimal(drawMl) + 1 / americanToDecimal(awayMl);
|
||||
}
|
||||
Reference in New Issue
Block a user