Files
vyndr/tests/unit/rankingInstrument.test.js
T
builtbykev ef4ac60b81 Per-sport rank guard + edge diagnostic-only display + delta report
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
2026-08-01 01:29:20 -04:00

114 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 MLBs 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']);
});
});