Model Train arc 1 (engine): de-vig + EV + takeable/value gates + hero v2 + triplet
Steps 1-6 — make "real opportunities at takeable prices" the engine, not a filter. 1. DE-VIG (src/utils/devig.js): two-way multiplicative de-vig strips the vig and returns fair prob + fair price per side + the overround. One side missing → fair UNAVAILABLE (null), never faked. Method noted in code + the `devig_method` field. 2. EV (devig.evPct): ev_pct = model prob × decimal − 1 at the graded side's ACTUAL price. This is the ranking signal now, replacing raw |model−consensus|. 3. TAKEABLE gate (src/config/valueEngine.js, TAKEABLE_ODDS_CEILING −160 .. +200, env-tunable): promoted surfaces only (hero/featured/alerts). The full board still shows everything; Parlay Lab exempt; JUICE_ODDS_FLOOR (−400) stays the absolute backstop underneath. Strict null-guard (Number(null)===0 would have made a missing price "takeable"). 4. VALUE flag: passes BOTH gates (takeable AND ev_pct ≥ VALUE_EV_THRESHOLD). Grade = read quality; value = the price pays you. Shipped in payloads. 5. HERO v2 (heroPropService): highest ev_pct among takeable A/B reads — a huge gap on a −900 line is trivia, not an opportunity. 6. VALUE TRIPLET: book_odds · fair_odds · model_odds on every read (snapshot, hero, scan — they all spread the grade). Handoff documents the fields; the rendering is Session-2 Design's job. All wired in analyzeViaEngine1's existing p_win/kelly block (real quantile probability × real book odds, or nothing). 33 new tests; suite 276/3306 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,14 @@ function toHero(g, sport, gap, isRecent) {
|
||||
gap: gap == null ? null : Math.round(gap * 100) / 100,
|
||||
team: g.team || null,
|
||||
reasoning: (g.reasoning && g.reasoning.summary) || null, // blurred paywall teaser
|
||||
// Model Train (steps 2/4/6) — EV ranking, the value flag, and the value
|
||||
// triplet (book price · fair de-vigged price · model price).
|
||||
ev_pct: g.ev_pct ?? null,
|
||||
value: g.value ?? null,
|
||||
takeable: g.takeable ?? null,
|
||||
book_odds: g.book_odds ?? (at.odds != null ? Number(at.odds) : null),
|
||||
fair_odds: g.fair_odds ?? null,
|
||||
model_odds: g.model_odds ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,14 +74,21 @@ async function pickHeroProp(deps = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
// The hero: largest |projection - line| among A/B candidates.
|
||||
let hero = null, heroGap = -1;
|
||||
// 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.
|
||||
const { isTakeable } = require('../config/valueEngine');
|
||||
let hero = null, heroEv = -Infinity;
|
||||
for (const { g, sport } of all) {
|
||||
if (!isAB(g.grade) || !candidate(g)) continue;
|
||||
const gap = Math.abs(Number(g.projection) - Number(g.line));
|
||||
if (gap > heroGap) { heroGap = gap; hero = { g, sport }; }
|
||||
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 }; }
|
||||
}
|
||||
if (hero) {
|
||||
const gap = candidate(hero.g) ? Math.abs(Number(hero.g.projection) - Number(hero.g.line)) : null;
|
||||
return toHero(hero.g, hero.sport, gap, false);
|
||||
}
|
||||
if (hero) return toHero(hero.g, hero.sport, heroGap, false);
|
||||
|
||||
// Empty slate → the MOST RECENT real graded read (any grade), by timestamp.
|
||||
let recent = null, recentTs = '';
|
||||
|
||||
Reference in New Issue
Block a user