proj-v1: book-implied from book_odds (grades carry odds, not fair_prob)
The live fingerprint showed proj_book_implied null on every real row: grades carry book_odds/locked_odds (e.g. -264) but NOT a de-vigged fair_prob, so keying the book comparison off fair_prob yielded null. The book ODDS are exactly "the book's implied probability" the handicapper test needs. Now proj_book_implied + the traded rung's book_implied derive from americanToImplied(book_odds), expressed on the OVER basis (under props → 1 - implied) so it's directly comparable to our P(≥rung). Vigged (a known offset the ledger measures both sides of). proj-v1 suites 24/24. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
This commit is contained in:
@@ -57,6 +57,13 @@ const abbrOf = (team) => {
|
|||||||
return NAME_TO_ABBR[s.toLowerCase()] || null;
|
return NAME_TO_ABBR[s.toLowerCase()] || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** American odds → implied probability (vigged). Null on absent/zero. */
|
||||||
|
function americanToImplied(odds) {
|
||||||
|
const o = num(odds);
|
||||||
|
if (o == null || o === 0) return null;
|
||||||
|
return o < 0 ? (-o) / (-o + 100) : 100 / (o + 100);
|
||||||
|
}
|
||||||
|
|
||||||
/** Park factor (multiplier ~1.0) for a team's home venue, or null. */
|
/** Park factor (multiplier ~1.0) for a team's home venue, or null. */
|
||||||
function parkFactorFor(teamAbbr) {
|
function parkFactorFor(teamAbbr) {
|
||||||
if (!teamAbbr) return null;
|
if (!teamAbbr) return null;
|
||||||
@@ -179,13 +186,19 @@ function projectProp({
|
|||||||
const rungs = dist.ladder(nb, LADDER_MAX);
|
const rungs = dist.ladder(nb, LADDER_MAX);
|
||||||
|
|
||||||
// ── LADDER + book implied (only the traded rung has a book number) ────────
|
// ── LADDER + book implied (only the traded rung has a book number) ────────
|
||||||
|
// The BOOK's implied probability comes from the book ODDS (grades carry
|
||||||
|
// book_odds, not a de-vigged fair_prob). This is the vigged implied — a known
|
||||||
|
// constant offset the ledger measures on both sides — expressed on the OVER
|
||||||
|
// basis so it's directly comparable to our P(≥ rung).
|
||||||
const tradedRung = isNum(line) ? Math.max(1, Math.ceil(line)) : null;
|
const tradedRung = isNum(line) ? Math.max(1, Math.ceil(line)) : null;
|
||||||
const fairProb = num(grade && grade.fair_prob); // de-vigged, graded direction
|
const bookOdds = num(grade && (grade.book_odds != null ? grade.book_odds : grade.locked_odds));
|
||||||
|
const rawImplied = americanToImplied(bookOdds);
|
||||||
|
const bookOverImplied = rawImplied == null ? null
|
||||||
|
: Math.round((direction === 'under' ? 1 - rawImplied : rawImplied) * 1000) / 1000;
|
||||||
const ladderOut = rungs.map((r) => ({
|
const ladderOut = rungs.map((r) => ({
|
||||||
rung: r.rung,
|
rung: r.rung,
|
||||||
p_at_least: r.p_at_least,
|
p_at_least: r.p_at_least,
|
||||||
book_implied: (tradedRung != null && r.rung === tradedRung && direction === 'over' && fairProb != null)
|
book_implied: (tradedRung != null && r.rung === tradedRung) ? bookOverImplied : null,
|
||||||
? Math.round(fairProb * 1000) / 1000 : null,
|
|
||||||
}));
|
}));
|
||||||
const pOverLine = tradedRung != null ? dist.round3(dist.nbSurvival(nb.r, nb.p, tradedRung)) : null;
|
const pOverLine = tradedRung != null ? dist.round3(dist.nbSurvival(nb.r, nb.p, tradedRung)) : null;
|
||||||
|
|
||||||
@@ -194,7 +207,7 @@ function projectProp({
|
|||||||
proj_point: point,
|
proj_point: point,
|
||||||
proj_line: line,
|
proj_line: line,
|
||||||
proj_p_over_line: pOverLine,
|
proj_p_over_line: pOverLine,
|
||||||
proj_book_implied: (direction === 'over' && fairProb != null) ? Math.round(fairProb * 1000) / 1000 : null,
|
proj_book_implied: bookOverImplied,
|
||||||
proj_distribution: {
|
proj_distribution: {
|
||||||
family: 'negative_binomial',
|
family: 'negative_binomial',
|
||||||
r: dist.round3(nb.r), p: dist.round3(nb.p),
|
r: dist.round3(nb.r), p: dist.round3(nb.p),
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ describe('projectProp — full object, never abstains, plausible', () => {
|
|||||||
const hitLog = (vals) => vals.map((h) => ({ isHome: true, opponent: 'Boston Red Sox', stat: { hits: h } }));
|
const hitLog = (vals) => vals.map((h) => ({ isHome: true, opponent: 'Boston Red Sox', stat: { hits: h } }));
|
||||||
|
|
||||||
it('emits distribution + full ladder + point + factor breakdown', () => {
|
it('emits distribution + full ladder + point + factor breakdown', () => {
|
||||||
const grade = { stat_type: 'hits', line: 0.5, direction: 'over', season_avg: 0.9, fair_prob: 0.62, team: 'NYY' };
|
// book_odds -150 → implied 0.6 (the book's number the grade actually carries)
|
||||||
|
const grade = { stat_type: 'hits', line: 0.5, direction: 'over', season_avg: 0.9, book_odds: -150, team: 'NYY' };
|
||||||
const p = pc.projectProp({ grade, gameLog: hitLog([1, 0, 2, 1, 1, 0, 1, 2, 1, 0]) });
|
const p = pc.projectProp({ grade, gameLog: hitLog([1, 0, 2, 1, 1, 0, 1, 2, 1, 0]) });
|
||||||
expect(p.proj_version).toBe('proj-v1');
|
expect(p.proj_version).toBe('proj-v1');
|
||||||
expect(p.proj_distribution.family).toBe('negative_binomial');
|
expect(p.proj_distribution.family).toBe('negative_binomial');
|
||||||
@@ -85,11 +86,19 @@ describe('projectProp — full object, never abstains, plausible', () => {
|
|||||||
expect(p.proj_factors.breakdown.map((f) => f.label)).toEqual(
|
expect(p.proj_factors.breakdown.map((f) => f.label)).toEqual(
|
||||||
expect.arrayContaining(['park_relative', 'weather', 'platoon', 'matchup']),
|
expect.arrayContaining(['park_relative', 'weather', 'platoon', 'matchup']),
|
||||||
);
|
);
|
||||||
// traded rung (ceil 0.5 = 1) carries the book-implied for comparison
|
// traded rung (ceil 0.5 = 1) carries the BOOK-implied (from odds) for comparison
|
||||||
expect(p.proj_ladder.find((r) => r.rung === 1).book_implied).toBeCloseTo(0.62, 3);
|
expect(p.proj_ladder.find((r) => r.rung === 1).book_implied).toBeCloseTo(0.6, 3);
|
||||||
|
expect(p.proj_book_implied).toBeCloseTo(0.6, 3);
|
||||||
expect(p.proj_p_over_line).toBeGreaterThan(0);
|
expect(p.proj_p_over_line).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('book implied is expressed on the OVER basis for an under prop', () => {
|
||||||
|
// under at -150 (implied 0.6 the ball stays under) → over-basis implied 0.4
|
||||||
|
const grade = { stat_type: 'hits', line: 1.5, direction: 'under', season_avg: 0.9, book_odds: -150, team: 'NYY' };
|
||||||
|
const p = pc.projectProp({ grade, gameLog: hitLog([1, 0, 2, 1, 1, 0, 1, 2, 1, 0]) });
|
||||||
|
expect(p.proj_book_implied).toBeCloseTo(0.4, 3);
|
||||||
|
});
|
||||||
|
|
||||||
it('NEVER abstains — an empty game log still projects (wide, from the prior)', () => {
|
it('NEVER abstains — an empty game log still projects (wide, from the prior)', () => {
|
||||||
const grade = { stat_type: 'hits', line: 0.5, direction: 'over', season_avg: 0.9, team: 'NYY' };
|
const grade = { stat_type: 'hits', line: 0.5, direction: 'over', season_avg: 0.9, team: 'NYY' };
|
||||||
const p = pc.projectProp({ grade, gameLog: [] });
|
const p = pc.projectProp({ grade, gameLog: [] });
|
||||||
|
|||||||
Reference in New Issue
Block a user