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:
@@ -496,24 +496,55 @@ async function analyzeViaEngine1(rawProp = {}) {
|
||||
}
|
||||
} catch { /* the ladder is additive — never breaks the read */ }
|
||||
|
||||
// Session 62 (A1-S1) — QUARTER-KELLY. Real probability (quantile estimator
|
||||
// over the actual game logs) × real book odds, or nothing. Never derived
|
||||
// from confidence, never a default vig.
|
||||
// Session 62 (A1-S1) — QUARTER-KELLY + Model Train (steps 1-6): de-vig, EV,
|
||||
// the value triplet, and the takeable/value flags. Real probability (quantile
|
||||
// estimator over the actual game logs) × real book odds, or nothing — never
|
||||
// derived from confidence, never a default vig.
|
||||
try {
|
||||
const { estimateProbability } = require('./probabilityEstimator');
|
||||
const { devigTwoWay, evPct, impliedProbToAmerican } = require('../../utils/devig');
|
||||
const { isTakeable, isValue } = require('../../config/valueEngine');
|
||||
|
||||
const dir = String(prop.direction || 'over').toLowerCase();
|
||||
const est = estimateProbability({ gameLogs: meta.gameLogs, line: prop.line, statType: rawProp.stat_type, features });
|
||||
const pWin = String(prop.direction || 'over').toLowerCase() === 'under'
|
||||
const pWin = dir === 'under'
|
||||
? (Number.isFinite(est.p_over) ? 1 - est.p_over : null)
|
||||
: (Number.isFinite(est.p_over) ? est.p_over : null);
|
||||
if (pWin != null) legacy.p_win = Math.round(pWin * 1000) / 1000;
|
||||
const sideOdds = String(prop.direction || 'over').toLowerCase() === 'under'
|
||||
? rawProp.under_odds : rawProp.over_odds;
|
||||
|
||||
const sideOdds = dir === 'under' ? rawProp.under_odds : rawProp.over_odds;
|
||||
|
||||
// Quarter-Kelly sizing (unchanged).
|
||||
if (pWin != null && sideOdds != null) {
|
||||
const { quarterKelly } = require('../../utils/kelly');
|
||||
const k = quarterKelly(pWin, sideOdds);
|
||||
if (k) legacy.kelly = { ...k, odds: String(sideOdds) };
|
||||
}
|
||||
} catch { /* sizing is additive — absent beats wrong */ }
|
||||
|
||||
// The VALUE TRIPLET (step 6): book price · fair (de-vigged) price · model
|
||||
// price. Two-way de-vig needs BOTH sides; one side missing → fair absent.
|
||||
if (sideOdds != null) legacy.book_odds = Number(sideOdds);
|
||||
const dv = devigTwoWay(rawProp.over_odds, rawProp.under_odds);
|
||||
if (dv) {
|
||||
const fair = dir === 'under' ? dv.under : dv.over;
|
||||
legacy.fair_prob = fair.fair_prob;
|
||||
legacy.fair_odds = fair.fair_odds;
|
||||
legacy.devig_method = dv.method;
|
||||
legacy.overround = dv.overround;
|
||||
}
|
||||
if (pWin != null) legacy.model_odds = impliedProbToAmerican(pWin);
|
||||
|
||||
// EV at the ACTUAL price (step 2) + the takeable/value flags (steps 3-4).
|
||||
// `takeable` is a property of the price alone; `value` also needs the edge.
|
||||
if (sideOdds != null) legacy.takeable = isTakeable(sideOdds);
|
||||
if (pWin != null && sideOdds != null) {
|
||||
const ev = evPct(pWin, sideOdds);
|
||||
if (ev != null) {
|
||||
legacy.ev_pct = ev;
|
||||
legacy.value = isValue(sideOdds, ev);
|
||||
}
|
||||
}
|
||||
} catch { /* the value layer is additive — absent beats wrong */ }
|
||||
|
||||
return legacy;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user