Fix: the incumbent ruler respects the allow-list (correcting my own model)
My first delta run modelled the incumbent as first-row-wins over the RAW feed and reported that an EXCLUDED book was "the market" on 69% of MLB prop-lines, with prizepicks alone at 47%. That is WRONG and I caught it before it went anywhere. normalizeProps applies ALLOWED_BOOKS BEFORE gradeSlateService.dedupeProps runs, so DFS books never reach the incumbent. The allow-list, for all the coverage it costs, does keep DFS out of the ruler. incumbentFairProb now takes the allow-list (defaulting to the live ALLOWED_BOOKS) and reproduces the real chain. Two tests lock it, including that a prop with no admitted book has NO incumbent -- it is never graded at all, which is the real loss and is already measured as invisible_props. Overstating the incumbent's badness would have been as dishonest as understating it, and more persuasive. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
@@ -124,15 +124,27 @@ function consensusFairProb(quotes, line, side, opts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CHALLENGER DELTA. The incumbent is "first row wins" — literally the first
|
* CHALLENGER DELTA — the incumbent, reproduced FAITHFULLY.
|
||||||
* quote in feed order, whatever book that is. This reproduces it faithfully so
|
*
|
||||||
* the comparison measures the real change and not an idealised one.
|
* The live chain is: PropLine -> `normalizeProps` (which applies the
|
||||||
|
* ALLOWED_BOOKS filter FIRST) -> `gradeSlateService.dedupeProps` (first row per
|
||||||
|
* player+stat+line wins). So the incumbent is the first quote from an ADMITTED
|
||||||
|
* book — not the first quote in the raw feed.
|
||||||
|
*
|
||||||
|
* This distinction is load-bearing and easy to get wrong in the alarming
|
||||||
|
* direction: ignoring the allow-list makes it look as though DFS pick'em has
|
||||||
|
* been pricing the model (prizepicks alone is 47% of raw first-rows). It has
|
||||||
|
* not — the allow-list, for all the coverage it costs, does keep DFS out of the
|
||||||
|
* incumbent. Overstating the incumbent's badness would be as dishonest as
|
||||||
|
* understating it.
|
||||||
*/
|
*/
|
||||||
function incumbentFairProb(quotes, line, side) {
|
function incumbentFairProb(quotes, line, side, allowedBooks) {
|
||||||
const want = String(side || 'over').toLowerCase() === 'under' ? 'under' : 'over';
|
const want = String(side || 'over').toLowerCase() === 'under' ? 'under' : 'over';
|
||||||
const target = num(line);
|
const target = num(line);
|
||||||
|
const allow = allowedBooks || require('../utils/oddsNormalizer').ALLOWED_BOOKS;
|
||||||
for (const q of quotes || []) {
|
for (const q of quotes || []) {
|
||||||
if (!q || !q.book) continue;
|
if (!q || !q.book) continue;
|
||||||
|
if (allow && !allow.has(String(q.book).toLowerCase())) continue;
|
||||||
if (target != null && num(q.line) !== target) continue;
|
if (target != null && num(q.line) !== target) continue;
|
||||||
const over = num(q.over_odds);
|
const over = num(q.over_odds);
|
||||||
const under = num(q.under_odds);
|
const under = num(q.under_odds);
|
||||||
@@ -150,7 +162,7 @@ function incumbentFairProb(quotes, line, side) {
|
|||||||
|
|
||||||
/** Both rulers plus the signed delta, for a single prop. */
|
/** Both rulers plus the signed delta, for a single prop. */
|
||||||
function compareRulers(quotes, line, side, opts = {}) {
|
function compareRulers(quotes, line, side, opts = {}) {
|
||||||
const incumbent = incumbentFairProb(quotes, line, side);
|
const incumbent = incumbentFairProb(quotes, line, side, opts.allowedBooks);
|
||||||
const consensus = consensusFairProb(quotes, line, side, opts);
|
const consensus = consensusFairProb(quotes, line, side, opts);
|
||||||
const delta = (incumbent.fair_prob != null && consensus.fair_prob != null)
|
const delta = (incumbent.fair_prob != null && consensus.fair_prob != null)
|
||||||
? round4(consensus.fair_prob - incumbent.fair_prob)
|
? round4(consensus.fair_prob - incumbent.fair_prob)
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ const REFERENCE_POLICIES = Object.freeze({
|
|||||||
* incumbent is first-row-wins, so it reports WHICH KIND of book has been acting
|
* incumbent is first-row-wins, so it reports WHICH KIND of book has been acting
|
||||||
* as "the market" — and DFS pick'em has the highest coverage in the feed.
|
* as "the market" — and DFS pick'em has the highest coverage in the feed.
|
||||||
*/
|
*/
|
||||||
function rulerDelta(raw) {
|
function rulerDelta(raw, allowed) {
|
||||||
const { compareRulers } = require('./consensusRuler');
|
const { compareRulers } = require('./consensusRuler');
|
||||||
const { roleOf } = require('../config/bookRoles');
|
const { roleOf } = require('../config/bookRoles');
|
||||||
|
|
||||||
@@ -289,6 +289,7 @@ function rulerDelta(raw) {
|
|||||||
|
|
||||||
const deltas = [];
|
const deltas = [];
|
||||||
const roleCounts = {};
|
const roleCounts = {};
|
||||||
|
let gradeable = 0;
|
||||||
let consensusAvailable = 0;
|
let consensusAvailable = 0;
|
||||||
let bothAvailable = 0;
|
let bothAvailable = 0;
|
||||||
let disagree2pts = 0;
|
let disagree2pts = 0;
|
||||||
@@ -296,9 +297,10 @@ function rulerDelta(raw) {
|
|||||||
|
|
||||||
for (const [key, quotes] of byPropLine.entries()) {
|
for (const [key, quotes] of byPropLine.entries()) {
|
||||||
const line = quotes[0].line;
|
const line = quotes[0].line;
|
||||||
const c = compareRulers(quotes, line, 'over');
|
const c = compareRulers(quotes, line, 'over', { allowedBooks: allowed });
|
||||||
if (c.consensus.source === 'consensus') consensusAvailable += 1;
|
if (c.consensus.source === 'consensus') consensusAvailable += 1;
|
||||||
if (c.incumbent.book) {
|
if (c.incumbent.book) {
|
||||||
|
gradeable += 1;
|
||||||
const r = roleOf(c.incumbent.book);
|
const r = roleOf(c.incumbent.book);
|
||||||
roleCounts[r] = (roleCounts[r] || 0) + 1;
|
roleCounts[r] = (roleCounts[r] || 0) + 1;
|
||||||
roleCounts[`book:${c.incumbent.book}`] = (roleCounts[`book:${c.incumbent.book}`] || 0) + 1;
|
roleCounts[`book:${c.incumbent.book}`] = (roleCounts[`book:${c.incumbent.book}`] || 0) + 1;
|
||||||
@@ -317,6 +319,8 @@ function rulerDelta(raw) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
prop_line_groups: byPropLine.size,
|
prop_line_groups: byPropLine.size,
|
||||||
|
gradeable_groups: gradeable,
|
||||||
|
gradeable_pct: byPropLine.size ? round2((100 * gradeable) / byPropLine.size) : null,
|
||||||
consensus_available: consensusAvailable,
|
consensus_available: consensusAvailable,
|
||||||
consensus_available_pct: byPropLine.size ? round2((100 * consensusAvailable) / byPropLine.size) : null,
|
consensus_available_pct: byPropLine.size ? round2((100 * consensusAvailable) / byPropLine.size) : null,
|
||||||
comparable: bothAvailable,
|
comparable: bothAvailable,
|
||||||
@@ -435,7 +439,7 @@ async function verify(opts = {}) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
out.per_sport[sport] = analyseBreadth(raw, allowed);
|
out.per_sport[sport] = analyseBreadth(raw, allowed);
|
||||||
out.per_sport[sport].ruler_delta = rulerDelta(raw);
|
out.per_sport[sport].ruler_delta = rulerDelta(raw, allowed);
|
||||||
const ev = raw.find((e) => e && e.id && Array.isArray(e.bookmakers) && e.bookmakers.length);
|
const ev = raw.find((e) => e && e.id && Array.isArray(e.bookmakers) && e.bookmakers.length);
|
||||||
if (ev) firstEvent[sport] = { id: ev.id, key: PA.SPORT_KEYS[sport] };
|
if (ev) firstEvent[sport] = { id: ev.id, key: PA.SPORT_KEYS[sport] };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -114,16 +114,26 @@ describe('consensusRuler — labelling and the median', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('consensusRuler — the incumbent it is challenging', () => {
|
describe('consensusRuler — the incumbent it is challenging', () => {
|
||||||
it('incumbent is literally first-row-wins, and that row can be a DFS book', () => {
|
const LIVE_ALLOWED = new Set(['draftkings', 'fanduel', 'betmgm', 'betrivers', 'pinnacle']);
|
||||||
const qs = [q('prizepicks', 1.5, -119, -119), q('novig', 1.5, -104, -104)];
|
|
||||||
const inc = incumbentFairProb(qs, 1.5, 'over');
|
it('incumbent is first-row-wins AMONG ADMITTED BOOKS — the allow-list runs first', () => {
|
||||||
expect(inc.book).toBe('prizepicks');
|
// normalizeProps applies ALLOWED_BOOKS before dedupeProps, so a DFS book
|
||||||
|
// sitting first in the raw feed is NOT the incumbent. Getting this wrong
|
||||||
|
// overstates the incumbent's badness, which is its own dishonesty.
|
||||||
|
const qs = [q('prizepicks', 1.5, -119, -119), q('betmgm', 1.5, -115, -105), q('draftkings', 1.5, -110, -110)];
|
||||||
|
const inc = incumbentFairProb(qs, 1.5, 'over', LIVE_ALLOWED);
|
||||||
|
expect(inc.book).toBe('betmgm');
|
||||||
expect(inc.ruler_version).toBe(roles.RULER_V1);
|
expect(inc.ruler_version).toBe(roles.RULER_V1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('a prop with NO admitted book has no incumbent at all — it is never graded', () => {
|
||||||
|
const qs = [q('prizepicks', 1.5, -119, -119), q('novig', 1.5, -104, -104)];
|
||||||
|
expect(incumbentFairProb(qs, 1.5, 'over', LIVE_ALLOWED).fair_prob).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('compareRulers returns a signed delta in probability points', () => {
|
it('compareRulers returns a signed delta in probability points', () => {
|
||||||
const qs = [q('draftkings', 1.5, -140, 120), q('novig', 1.5, -104, -104), q('kalshi', 1.5, -103, -105)];
|
const qs = [q('draftkings', 1.5, -140, 120), q('novig', 1.5, -104, -104), q('kalshi', 1.5, -103, -105)];
|
||||||
const c = compareRulers(qs, 1.5, 'over');
|
const c = compareRulers(qs, 1.5, 'over', { allowedBooks: LIVE_ALLOWED });
|
||||||
expect(c.incumbent.book).toBe('draftkings');
|
expect(c.incumbent.book).toBe('draftkings');
|
||||||
expect(c.consensus.source).toBe('consensus');
|
expect(c.consensus.source).toBe('consensus');
|
||||||
expect(c.delta_pts).toBeLessThan(0); // dk's favourite priced over above the exchanges
|
expect(c.delta_pts).toBeLessThan(0); // dk's favourite priced over above the exchanges
|
||||||
@@ -131,6 +141,6 @@ describe('consensusRuler — the incumbent it is challenging', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('delta is null when either side is unavailable — never 0', () => {
|
it('delta is null when either side is unavailable — never 0', () => {
|
||||||
expect(compareRulers([q('prizepicks', 1.5, -119, -119)], 1.5, 'over').delta_pts).toBeNull();
|
expect(compareRulers([q('prizepicks', 1.5, -119, -119)], 1.5, 'over', { allowedBooks: LIVE_ALLOWED }).delta_pts).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user