ef4ac60b81
DELTA MEASURED on live prod grades (live ordering unchanged): MLB 7/8 props move (87.5%), mean 2.5 places, TOP READ CHANGES (corey seager hits 1.5 under -> jake burger hits 0.5 over). WNBA 25/25 move, mean 4.1, max 12. This is a large re-ordering, not a tweak. Caveat recorded rather than buried: MLB had only 8 graded props at measurement time. The percentages are real; the sample is one small slate. Re-run before the flip -- it is one call. PER-SPORT DOCTRINE ENFORCED IN CODE. WNBA moves the most and must NOT adopt this: its p_win is anti-predictive, so ranking that board by p_win would sort it by a signal measured to point the WRONG WAY -- worse than the incumbent, not better. A comment would not have stopped a future flip from going global, so FORECAST_RANKED_SPORTS = Set(['mlb']) gates the forecast_rank stamp, with tests asserting no sport inherits MLB's result. A sport joins only by passing its own holdout. EDGE IS NOW DIAGNOSTIC-ONLY IN DISPLAY. MobileEdgeBoard.EdgeCell rendered green (--g-a) for positive edge and red (--miss) for negative. Two things were wrong: green/red IS a quality claim on a quantity that does not predict, and ROW-GRAMMAR reserves red for settled-negative ONLY -- a negative diagnostic is not a settled loss. Now neutral mono with a diagnostic tooltip; header reads "MKT GAP · DIAGNOSTIC". The number is still shown -- no display went blank. DeskShowcase neutralised likewise. PINNACLE LOGGED, NOT ENSHRINED. Per the order, "market-not-sharp" is PENDING-RECOVERY rather than a confirmed permanent limitation. The single question for PropLine is in BLOCKERS.md with its evidence, and MASTER-PLAN now carries the pending status instead of the permanent claim. Live sorts remain byte-identical: selectTopGrades, flattenToEdgeBoard and topGradedService all still call the incumbent. Gates: 4,041 tests / 323 suites green; next build exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
114 lines
4.6 KiB
JavaScript
114 lines
4.6 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* Ranking instrument — challenger vs incumbent (2026-08-01).
|
||
*
|
||
* These lock the reason the challenger exists: measured on n=200 settled MLB
|
||
* rows, corr(p_win, outcome) = +0.26 while corr(edge, outcome) = -0.010 under
|
||
* the incumbent ruler and -0.022 under the consensus ruler. A quantity that
|
||
* does not predict must not rank, gate or decide.
|
||
*/
|
||
|
||
const {
|
||
rankGrades, rankByForecast, rankingDelta, gradeKey, takeablePWin, ranksOnForecast, FORECAST_RANKED_SPORTS,
|
||
} = require('../../src/utils/gradeRanking');
|
||
|
||
const g = (player, grade, p_win, odds = -110, extra = {}) => ({
|
||
player, stat_type: 'hits', line: 1.5, direction: 'over',
|
||
grade, p_win, book_odds: odds, ...extra,
|
||
});
|
||
|
||
describe('rankByForecast — the challenger', () => {
|
||
it('contains NO edge term: edge cannot move the order at all', () => {
|
||
const a = [g('A', 'B', 0.62, -110, { edge: -99 }), g('B', 'B', 0.55, -110, { edge: +99 })];
|
||
const b = [g('A', 'B', 0.62, -110, { edge: +99 }), g('B', 'B', 0.55, -110, { edge: -99 })];
|
||
expect(rankByForecast(a).map((x) => x.player)).toEqual(['A', 'B']);
|
||
expect(rankByForecast(b).map((x) => x.player)).toEqual(['A', 'B']);
|
||
});
|
||
|
||
it('leads with p_win, not the grade letter', () => {
|
||
// The letter measured r ~ 0.005 and is INVERTED; p_win measures +0.26.
|
||
// A high-p_win C must outrank a low-p_win A.
|
||
const out = rankByForecast([g('lowPwinA', 'A', 0.51), g('highPwinC', 'C', 0.74)]);
|
||
expect(out[0].player).toBe('highPwinC');
|
||
});
|
||
|
||
it('keeps the takeable gate — raw p_win would crown chalk', () => {
|
||
const chalk = g('chalk', 'A', 0.93, -300); // untakeable price
|
||
const real = g('real', 'B', 0.61, -115);
|
||
expect(takeablePWin(chalk)).toBeNull();
|
||
expect(rankByForecast([chalk, real])[0].player).toBe('real');
|
||
});
|
||
|
||
it('sorts a missing p_win LAST, never first (Number(null) === 0 guard)', () => {
|
||
const out = rankByForecast([g('none', 'A', null), g('has', 'C', 0.58)]);
|
||
expect(out.map((x) => x.player)).toEqual(['has', 'none']);
|
||
});
|
||
|
||
it('is stable for genuinely tied rows', () => {
|
||
const rows = [g('first', 'B', 0.6), g('second', 'B', 0.6)];
|
||
expect(rankByForecast(rows).map((x) => x.player)).toEqual(['first', 'second']);
|
||
});
|
||
|
||
it('drops ungraded rows, like the incumbent', () => {
|
||
expect(rankByForecast([g('x', null, 0.9), g('y', 'B', 0.5)]).map((r) => r.player)).toEqual(['y']);
|
||
});
|
||
});
|
||
|
||
describe('rankGrades — the incumbent is UNTOUCHED (live ordering byte-identical)', () => {
|
||
it('still leads with the grade letter and still consults edge', () => {
|
||
const out = rankGrades([g('lowPwinA', 'A', 0.51), g('highPwinC', 'C', 0.74)]);
|
||
expect(out[0].player).toBe('lowPwinA'); // grade-first, unchanged
|
||
});
|
||
|
||
it('edge still breaks a true tie in the incumbent', () => {
|
||
const out = rankGrades([
|
||
g('lowEdge', 'B', 0.6, -110, { edge: 1 }),
|
||
g('highEdge', 'B', 0.6, -110, { edge: 9 }),
|
||
]);
|
||
expect(out[0].player).toBe('highEdge');
|
||
});
|
||
});
|
||
|
||
describe('rankingDelta — the challenger-first measurement', () => {
|
||
it('reports how far the board moves and whether the top read changes', () => {
|
||
const rows = [g('A', 'A', 0.52), g('B', 'C', 0.77), g('C', 'B', 0.64)];
|
||
const d = rankingDelta(rows, 3);
|
||
expect(d.n).toBe(3);
|
||
expect(d.incumbent_top).toBe(gradeKey(rows[0])); // A-grade leads incumbent
|
||
expect(d.challenger_top).toBe(gradeKey(rows[1])); // highest p_win leads challenger
|
||
expect(d.top_changed).toBe(true);
|
||
expect(d.moved).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('reports zero movement when both instruments agree', () => {
|
||
const rows = [g('A', 'A', 0.80), g('B', 'B', 0.60), g('C', 'C', 0.40)];
|
||
const d = rankingDelta(rows, 3);
|
||
expect(d.moved).toBe(0);
|
||
expect(d.top_changed).toBe(false);
|
||
expect(d.top_n_overlap_pct).toBe(100);
|
||
});
|
||
|
||
it('changes nothing about the inputs (pure)', () => {
|
||
const rows = [g('A', 'A', 0.52), g('B', 'C', 0.77)];
|
||
const snapshot = JSON.stringify(rows);
|
||
rankingDelta(rows);
|
||
expect(JSON.stringify(rows)).toBe(snapshot);
|
||
});
|
||
});
|
||
|
||
describe('per-sport doctrine — who may rank on the forecast', () => {
|
||
it('MLB may; WNBA may NOT (its p_win is anti-predictive, it abstains)', () => {
|
||
expect(ranksOnForecast('mlb')).toBe(true);
|
||
expect(ranksOnForecast('MLB')).toBe(true);
|
||
expect(ranksOnForecast('wnba')).toBe(false);
|
||
});
|
||
|
||
it('no sport inherits MLB’s result — unknown sports are excluded', () => {
|
||
for (const s of ['nba', 'nfl', 'soccer', 'nhl', 'ncaab', '', null, undefined]) {
|
||
expect(ranksOnForecast(s)).toBe(false);
|
||
}
|
||
expect([...FORECAST_RANKED_SPORTS]).toEqual(['mlb']);
|
||
});
|
||
});
|