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:
@@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Value engine config (Model Train, steps 3-4).
|
||||
*
|
||||
* Product doctrine (Kev): VYNDR PROMOTES bets people actually take — roughly the
|
||||
* -160 to +200 band — that ALSO carry a genuine vig-free edge. Not
|
||||
* plus-money-only, not heavy chalk.
|
||||
*
|
||||
* The TAKEABLE gate applies to PROMOTED surfaces only (daily hero, featured /
|
||||
* top-of-board, future alerts). The full board still shows every graded read.
|
||||
* Parlay Lab is EXEMPT (juiced legs combine into takeable payouts).
|
||||
* JUICE_ODDS_FLOOR (-400, in rareEventMarkets) stays the absolute refusal
|
||||
* backstop UNDERNEATH this — those reads are never graded at all.
|
||||
*
|
||||
* All thresholds are env-tunable.
|
||||
*/
|
||||
|
||||
// The promotable price band. Ceiling = most-juiced favorite we'll promote;
|
||||
// max = longest dog we'll promote.
|
||||
const TAKEABLE_ODDS_CEILING = Number(process.env.TAKEABLE_ODDS_CEILING || -160);
|
||||
const TAKEABLE_ODDS_MAX = Number(process.env.TAKEABLE_ODDS_MAX || 200);
|
||||
|
||||
// A read is VALUE only when its EV clears this (a real edge, not rounding).
|
||||
const VALUE_EV_THRESHOLD = Number(process.env.VALUE_EV_THRESHOLD || 2);
|
||||
|
||||
/** Is an American price inside the takeable band (default -160..+200)?
|
||||
* Strict on input — null/''/undefined are NOT takeable (Number(null) === 0
|
||||
* would otherwise land a missing price inside the band). */
|
||||
function isTakeable(american) {
|
||||
if (american == null || american === '') return false;
|
||||
const a = Number(american);
|
||||
if (!Number.isFinite(a)) return false;
|
||||
return a >= TAKEABLE_ODDS_CEILING && a <= TAKEABLE_ODDS_MAX;
|
||||
}
|
||||
|
||||
/** A read carries VALUE when it's takeable AND its EV clears the threshold —
|
||||
* "the price pays you to take it", distinct from grade ("read quality"). */
|
||||
function isValue(american, evPct) {
|
||||
return isTakeable(american) && Number.isFinite(evPct) && evPct >= VALUE_EV_THRESHOLD;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
TAKEABLE_ODDS_CEILING,
|
||||
TAKEABLE_ODDS_MAX,
|
||||
VALUE_EV_THRESHOLD,
|
||||
isTakeable,
|
||||
isValue,
|
||||
};
|
||||
Reference in New Issue
Block a user