Rank on p_win: challenger instrument + retire edge from decisions
MEASURED BASIS (n=200 settled MLB rows): corr(p_win, outcome) = +0.26; corr(edge, outcome) = -0.010 incumbent ruler / -0.022 consensus ruler. Subtracting the market destroys the signal under BOTH rulers, so a quantity that does not predict must not rank, gate or decide. CHALLENGER-FIRST -- live ordering is byte-identical. rankGrades (the incumbent, grade-first with edge as its 4th key) is untouched and tested as untouched. NEW: rankByForecast -- takeable-gated p_win -> grade -> confidence -> stable order, with NO edge term anywhere. p_win LEADS and the letter follows, deliberately: the letter measured r ~ 0.005 and is inverted (B 52.4% < C 56.9%) while p_win measures +0.26, so leading with the letter would sort by the weaker signal and use the stronger one only to break ties. Recorded in the code: isotonic calibration is a MONOTONE transform, so ranking on raw vs calibrated p_win gives the SAME ORDER. Calibration matters when p_win is displayed or thresholded; it cannot change a ranking. Nothing here needs the calibrated value. rankingDelta + GET /api/internal/ranking-delta measure how far the board would move before any flip. The endpoint reports p_win coverage alongside the delta -- if p_win is absent the challenger degrades to grade order and the delta UNDERSTATES, which is worth saying rather than reporting a clean zero. forecast_rank is stamped on snapshot grades BEFORE stripModelPrice, so every tier gets the correct order without the paid values (the topGradedService precedent -- an ordinal can travel where the magnitude cannot). Additive only: nothing sorts by it yet. RETIRED AS DECISIONS (not rankings, so done now): - altLineScanner.compareToBookImplied no longer returns value_detected: edge > 0. Edge is still COMPUTED and returned -- losing the record would be worse than mis-using it -- but the verdict is an honest null with value_basis: 'retired:edge_does_not_predict'. - scanAltLines no longer filters to edge>0 or calls the survivor "optimal". The whole ladder is returned ranked and labelled 'price_gap_diagnostic_unvalidated'. The module has ZERO callers (verified) -- unwired like mlbGrader.js, left in place and made honest. An honest asymmetry recorded there: ranking props AGAINST EACH OTHER must not use edge, but choosing between RUNGS OF THE SAME PROP is inherently price-relative -- ranking rungs by model probability alone would always pick the lowest line, since P(over 0.5) > P(over 2.5) by construction. So the gap stays the rung key, explicitly labelled unvalidated. Two superseded tests updated to stronger properties. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
@@ -556,4 +556,49 @@ router.get('/propline-verify', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/internal/ranking-delta (Order: rank on p_win — CHALLENGER-FIRST)
|
||||
*
|
||||
* Reads the live snapshot and reports how far the board WOULD move if the
|
||||
* ranking instrument changed from `rankGrades` (grade-first, edge as 4th key)
|
||||
* to `rankByForecast` (p_win-first, no edge term). Changes nothing — the live
|
||||
* ordering is untouched until this delta is reviewed.
|
||||
*
|
||||
* ?sports=mlb,wnba ?top=10
|
||||
*/
|
||||
router.get('/ranking-delta', async (req, res) => {
|
||||
try {
|
||||
const { cacheGet } = require('../utils/redis');
|
||||
const { rankingDelta } = require('../utils/gradeRanking');
|
||||
const sports = String(req.query.sports || 'mlb,wnba')
|
||||
.split(',').map((x) => x.trim().toLowerCase()).filter(Boolean).slice(0, 6);
|
||||
const topN = Math.max(1, Math.min(50, parseInt(req.query.top, 10) || 10));
|
||||
|
||||
const out = {};
|
||||
for (const sport of sports) {
|
||||
let grades = null;
|
||||
const snap = await cacheGet(`snapshot:${sport}:latest`);
|
||||
if (snap && Array.isArray(snap.grades)) grades = snap.grades;
|
||||
else {
|
||||
const env = await cacheGet(`grades:${sport}`);
|
||||
if (env && Array.isArray(env.grades)) grades = env.grades;
|
||||
}
|
||||
if (!grades || grades.length === 0) { out[sport] = { note: 'no cached grades' }; continue; }
|
||||
const withPWin = grades.filter((g) => g && g.p_win != null).length;
|
||||
out[sport] = {
|
||||
graded: grades.length,
|
||||
with_p_win: withPWin,
|
||||
// Honest: if p_win is absent the challenger degrades to grade order and
|
||||
// the delta understates. Say so rather than reporting a clean zero.
|
||||
p_win_coverage_pct: grades.length ? Math.round((1000 * withPWin) / grades.length) / 10 : null,
|
||||
...rankingDelta(grades, topN),
|
||||
};
|
||||
}
|
||||
res.set('Cache-Control', 'no-store');
|
||||
return res.json({ ok: true, live_ordering_unchanged: true, per_sport: out });
|
||||
} catch (err) {
|
||||
return res.status(500).json({ ok: false, error: err && err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
+22
-1
@@ -20,6 +20,7 @@ const { indexRosterLogs, attachLast10Dots } = require('../services/last10Dots');
|
||||
// viewer. This endpoint is PUBLIC, so the Session-66 gate on /api/analyze was
|
||||
// being bypassed here on every graded row. Same layer as the CLV gate.
|
||||
const { stripModelPrice, gateItemizedGrades, liveLockedSummary, freeSample, entitledToItemizedGrades } = require('../utils/snapshotGating');
|
||||
const { rankByForecast, gradeKey } = require('../utils/gradeRanking');
|
||||
const { resolveTierFromRequest } = require('../utils/requestTier');
|
||||
|
||||
const router = express.Router();
|
||||
@@ -108,7 +109,27 @@ router.get('/:sport', async (req, res) => {
|
||||
// The earlier resolution-flip freed settled grades, which made the free tier a
|
||||
// ONE-DAY-DELAYED FEED of the whole product. Order still matters: strip the model
|
||||
// PRICE first (S67), then withhold judgment on EVERY itemized grade.
|
||||
const gate = (grades) => gateItemizedGrades(stripModelPrice(grades, tier), tier);
|
||||
// FORECAST RANK (Order: rank on p_win, 2026-08-01) — stamped BEFORE the
|
||||
// model-price strip, so every tier receives the CORRECT ORDER without the
|
||||
// paid values. Same precedent as topGradedService: `p_win` is stripped for
|
||||
// unentitled callers, so a client cannot rank on it; an ordinal can travel
|
||||
// where the magnitude cannot.
|
||||
//
|
||||
// ADDITIVE ONLY IN THIS ORDER. Nothing sorts by it yet — the live ordering
|
||||
// is byte-identical until the flip is reviewed against the recorded delta.
|
||||
// It leaks ordering, not magnitude, which is the same trade already made
|
||||
// and accepted for the top-graded board.
|
||||
const stampForecastRank = (grades) => {
|
||||
if (!Array.isArray(grades) || grades.length === 0) return grades;
|
||||
const ranked = rankByForecast(grades);
|
||||
const pos = new Map();
|
||||
ranked.forEach((g, i) => pos.set(gradeKey(g), i + 1));
|
||||
return grades.map((g) => {
|
||||
const r = pos.get(gradeKey(g));
|
||||
return r == null ? g : { ...g, forecast_rank: r };
|
||||
});
|
||||
};
|
||||
const gate = (grades) => gateItemizedGrades(stripModelPrice(stampForecastRank(grades), tier), tier);
|
||||
// Free proof, none of it itemizing the nightly slate:
|
||||
// - the tease: AGGREGATE count + tier shape, computed from the ungated rows and
|
||||
// never joined back to one, so nobody can tell WHICH prop is the A
|
||||
|
||||
Reference in New Issue
Block a user