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:
@@ -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 } };
|
||||
|
||||
Reference in New Issue
Block a user