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>