Hero ranking fix: rank by champion p_win among takeable, honest empty state

Review Zero found the hero's ACTUAL behavior was worse than "unknown": it ranks
on ev_pct (heroPropService v2), but ev_pct is NULL on served grades and
Number(null)===0 made Number.isFinite(Number(null)) TRUE — so every prop tied at
EV 0 and the "top read" was really the FIRST takeable A/B prop in cache order
(arbitrary, dressed as ranked).

v3: rank by the CHAMPION's p_win (the only signal with a promising, not proven,
edge — its takeable-MLB-over CLV survived the skew audit) among A/B, TAKEABLE-
priced reads (isTakeable band -160..+200, same as the proof/audit). Strict
null guard kills the Number(null)=0 bug. Takeable filter is mandatory (raw p_win
crowns -300 chalk). NO backfill: nothing qualifies → honest empty state
(available:false, reason:'no_qualifying_read'), never a weak recent read.

p_win is RANKING-ONLY, server-side — toHero never exposes it and the route strips
it. Framing unchanged in substance (model number vs book number, grade,
timestamp) — no proven-edge / +EV / best-bet claim, no CLV/ROI/edge number.
Display-only: reads snapshot caches, writes to nothing (no grade/ledger/lock_lines).
Full suite 3852 green, web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1
This commit is contained in:
Kev
2026-07-29 04:43:21 -04:00
parent e7ec501054
commit 41b86e3874
3 changed files with 72 additions and 57 deletions
+27 -21
View File
@@ -74,35 +74,41 @@ async function pickHeroProp(deps = {}) {
}
}
// HERO RULE v2 (Model Train, step 5): the highest EV among reads that pass the
// TAKEABLE gate, A/B grades only. A huge model-vs-line gap on a -900 line is
// trivia; the hero is the best OPPORTUNITY at a price you'd actually take.
// HERO RULE v3 (hero-ranking-fix, 2026-07-29). Rank by the CHAMPION's p_win among
// TAKEABLE-priced A/B reads — the highest-confidence read at a price you'd take.
//
// - Why not ev_pct (the v2 key): ev_pct is NULL on served grades, and
// `Number(null) === 0` made `Number.isFinite(Number(g.ev_pct))` TRUE, so every
// prop tied at EV 0 and the "top read" was really the FIRST takeable A/B prop in
// cache order — arbitrary, dressed as ranked. The `num()` guard below kills that.
// - p_win is the only signal with a *promising* (not proven) edge (the champion's
// takeable-MLB-over CLV survived the skew audit). It is used for RANKING ONLY,
// server-side; it is never exposed (toHero omits it; the route strips it too).
// - TAKEABLE filter is MANDATORY: raw p_win crowns -300 chalk, which is not the
// product. Floor = the project's `isTakeable` band (-160..+200), same definition
// the takeable-edge proof/audit used.
// - NO BACKFILL: nothing qualifies → honest empty state, never a weak recent read.
const { isTakeable } = require('../config/valueEngine');
let hero = null, heroEv = -Infinity;
// Strict null: `Number(null) === 0` is the exact fabrication this hero fell to.
const num = (v) => (v == null || v === '' ? null : (Number.isFinite(Number(v)) ? Number(v) : null));
let hero = null; let heroP = -Infinity;
for (const { g, sport } of all) {
if (!isAB(g.grade) || !candidate(g)) continue;
if (!Number.isFinite(Number(g.ev_pct))) continue; // need a real EV to rank
if (!isTakeable(g.book_odds)) continue; // promoted surface → takeable only
if (Number(g.ev_pct) > heroEv) { heroEv = Number(g.ev_pct); hero = { g, sport }; }
const p = num(g.p_win);
if (p == null) continue; // no champion p_win → cannot rank
const price = num(g.book_odds) ?? num(g.gradedAt && g.gradedAt.odds);
if (price == null || !isTakeable(price)) continue; // takeable price only — excludes chalk
if (p > heroP) { heroP = p; hero = { g, sport }; }
}
if (hero) {
const gap = candidate(hero.g) ? Math.abs(Number(hero.g.projection) - Number(hero.g.line)) : null;
const gap = Math.abs(Number(hero.g.projection) - Number(hero.g.line));
return toHero(hero.g, hero.sport, gap, false);
}
// Empty slate → the MOST RECENT real graded read (any grade), by timestamp.
let recent = null, recentTs = '';
for (const { g, sport } of all) {
const ts = (g.gradedAt && g.gradedAt.timestamp) || '';
if (ts && ts > recentTs) { recentTs = ts; recent = { g, sport }; }
}
if (recent) {
const gp = candidate(recent.g) ? Math.abs(Number(recent.g.projection) - Number(recent.g.line)) : null;
return toHero(recent.g, recent.sport, gp, true);
}
// Truly nothing cached — the card hides. Never a fabricated fallback.
return { available: false };
// HONEST EMPTY STATE — no takeable high-p_win read qualifies (or the slate is empty).
// Deliberately NO recent-read backfill: a weak read dressed as today's hero is exactly
// the dishonesty this order removes. The card self-hides.
return { available: false, reason: 'no_qualifying_read' };
}
module.exports = { pickHeroProp, __internals: { isAB, candidate, DEFAULT_SPORTS } };
+38 -29
View File
@@ -1,76 +1,85 @@
'use strict';
// Hero rule v2 (Model Train, step 5): highest ev_pct among reads passing the
// TAKEABLE gate, A/B grades only. Empty slate → most recent real read.
// Hero rule v3 (hero-ranking-fix, 2026-07-29): rank by the CHAMPION's p_win among
// TAKEABLE-priced A/B reads. No ev_pct (NULL on served grades). No backfill — nothing
// qualifies → honest empty state.
const { pickHeroProp } = require('../../src/services/heroPropService');
function cacheFrom(map) { return async (key) => (key in map ? map[key] : null); }
// A graded read carries ev_pct + book_odds (the v2 ranking inputs).
const grade = (o) => ({
player_name: o.player, stat_type: o.stat || 'hits', line: o.line ?? 1.5, projection: o.proj ?? 2.0,
direction: o.dir || 'over', grade: o.grade, book: o.book || 'dk',
ev_pct: o.ev, book_odds: o.odds ?? -120, value: o.value ?? null,
p_win: o.pwin, ev_pct: o.ev ?? null, book_odds: o.odds ?? -120, value: o.value ?? null,
fair_odds: o.fair ?? null, model_odds: o.model ?? null,
gradedAt: { line: o.line ?? 1.5, odds: o.odds ?? -120, timestamp: o.ts || '2026-07-17T19:00:00Z' },
});
describe('pickHeroProp — v2 (EV among takeable A/B)', () => {
test('picks the HIGHEST ev_pct among takeable A/B reads', async () => {
describe('pickHeroProp — v3 (champion p_win among takeable A/B)', () => {
test('picks the HIGHEST p_win among takeable A/B reads', async () => {
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
grade({ player: 'LowEV', grade: 'A', ev: 3.1, odds: -120 }),
grade({ player: 'HighEV', grade: 'B', ev: 8.4, odds: +110 }),
grade({ player: 'LowP', grade: 'A', pwin: 0.55, odds: -120 }),
grade({ player: 'HighP', grade: 'B', pwin: 0.71, odds: 110 }),
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.available).toBe(true);
expect(hero.player).toBe('HighEV');
expect(hero.ev_pct).toBe(8.4);
expect(hero.player).toBe('HighP');
expect(hero.p_win).toBeUndefined(); // p_win is RANKING-ONLY, never exposed on the hero
});
test('a HUGE EV on an un-takeable price (-900) is NOT the hero (trivia, not opportunity)', async () => {
test('un-takeable chalk is EXCLUDED even with the highest p_win (raw p_win crowns chalk)', async () => {
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
grade({ player: 'Chalk', grade: 'A', ev: 20, odds: -900 }), // un-takeable
grade({ player: 'Takeable', grade: 'B', ev: 5, odds: -130 }), // in band
grade({ player: 'Chalk', grade: 'A', pwin: 0.90, odds: -300 }), // outside -160..+200
grade({ player: 'Takeable', grade: 'B', pwin: 0.60, odds: -130 }),
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.player).toBe('Takeable');
});
test('C/D/F grades are ineligible even with high EV', async () => {
test('C/D/F grades ineligible even with high p_win', async () => {
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
grade({ player: 'HighEV_C', grade: 'C', ev: 12, odds: -110 }),
grade({ player: 'RealA', grade: 'A', ev: 4, odds: -110 }),
grade({ player: 'HighP_C', grade: 'C', pwin: 0.80, odds: -110 }),
grade({ player: 'RealA', grade: 'A', pwin: 0.60, odds: -110 }),
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.player).toBe('RealA');
});
test('exposes the value triplet + ev/value on the hero', async () => {
test('Number(null)===0 GUARD: a null-p_win prop never ranks (no arbitrary hero)', async () => {
// The v2 bug: Number(null)===0 → every null-EV prop tied at 0 → the first one won.
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
{ player_name: 'Trip', stat_type: 'hits', line: 1.5, projection: 2.1, direction: 'over',
grade: 'A', book: 'dk', ev_pct: 6.2, value: true, takeable: true,
book_odds: -145, fair_odds: -132, model_odds: -110,
gradedAt: { line: 1.5, odds: -145, timestamp: '2026-07-17T19:00:00Z' } },
grade({ player: 'NullP', grade: 'A', pwin: null, odds: -120 }), // must NOT win
grade({ player: 'RealP', grade: 'A', pwin: 0.58, odds: -120 }),
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.player).toBe('RealP');
// and when EVERY prop has null p_win → honest empty state, not an arbitrary pick
const allNull = cacheFrom({ 'snapshot:mlb:latest': { grades: [grade({ player: 'X', grade: 'A', pwin: null, odds: -120 })] } });
expect((await pickHeroProp({ cacheGet: allNull, sports: ['mlb'] })).available).toBe(false);
});
test('exposes book/fair/model odds CONTEXT on the hero (display, not ranking)', async () => {
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
grade({ player: 'Trip', grade: 'A', pwin: 0.64, odds: -145, fair: -132, model: -110 }),
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.book_odds).toBe(-145);
expect(hero.fair_odds).toBe(-132);
expect(hero.model_odds).toBe(-110);
expect(hero.ev_pct).toBe(6.2);
expect(hero.value).toBe(true);
});
test('no takeable A/B EV read → most recent real read (fallback)', async () => {
test('no takeable A/B p_win read → HONEST EMPTY STATE, never a backfilled recent read', async () => {
const cacheGet = cacheFrom({ 'snapshot:mlb:latest': { grades: [
grade({ player: 'OnlyChalk', grade: 'A', ev: 9, odds: -800, ts: '2026-07-17T19:00:00Z' }),
grade({ player: 'OnlyChalk', grade: 'A', pwin: 0.9, odds: -800 }), // un-takeable
] } });
const hero = await pickHeroProp({ cacheGet, sports: ['mlb'] });
expect(hero.available).toBe(true);
expect(hero.is_recent).toBe(true);
expect(hero.player).toBe('OnlyChalk');
expect(hero.available).toBe(false);
expect(hero.reason).toBe('no_qualifying_read');
expect(hero.is_recent).toBeUndefined(); // NO recent-read backfill
});
test('nothing cached → { available:false }', async () => {
const hero = await pickHeroProp({ cacheGet: cacheFrom({}), sports: ['mlb'] });
expect(hero).toEqual({ available: false });
expect(hero.available).toBe(false);
});
});
+7 -7
View File
@@ -7,13 +7,13 @@ import PriceTriplet from './vyndr/PriceTriplet';
/**
* Daily hero prop card (Truth-Everywhere Part 2, item 5).
*
* A live RULE, not a hand-picked example: the graded prop where VYNDR disagrees
* MOST with the market — largest |model - consensus| gap, A/B grades only —
* fetched from /api/hero-prop (which reads the pre-graded snapshot). The card
* shows the disagreement explicitly (the book's number vs ours) with the real
* grade timestamp. Empty slate → the most recent real graded read. Nothing
* cached → the card hides. There is NO static fallback — the old Jokic
* "Example" card is gone.
* A live RULE, not a hand-picked example (hero-ranking-fix, 2026-07-29): the
* HIGHEST-CONFIDENCE read at a takeable price — the champion's top p_win among
* A/B, takeable-priced props — fetched from /api/hero-prop (which reads the
* pre-graded snapshot). The card shows the model's number vs the book's, with the
* real grade timestamp — a high-confidence read, NOT a claim of proven edge / +EV /
* best bet (no CLV/ROI/edge number is shown). Nothing qualifies → the card HIDES
* (honest empty state — no backfilled weak read, no static "Example" fallback).
*/
type Hero = {