Files
vyndr/src/utils/devig.js
T
builtbykev 7a925f43eb 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>
2026-07-19 02:43:30 -04:00

82 lines
3.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* De-vig engine (Model Train, step 1) — the foundation for EV + value.
*
* A two-sided prop price carries the book's vig (the "overround"): the two
* sides' implied probabilities sum to MORE than 1. De-vigging strips that back
* out to FAIR probabilities that sum to 1, and converts them back to a fair
* (vig-free) price per side.
*
* METHOD: multiplicative / proportional de-vig — each side's implied prob is
* divided by the total. It's the standard, distribution-free two-way method
* (a.k.a. "normalized implied probability"). Simple, transparent, and correct
* for two-way markets; we note it in the stored `method` field.
*
* If only one side is priced, fair values are UNAVAILABLE (null) — never faked.
*/
const round3 = (n) => Math.round(n * 1000) / 1000;
/** American odds → implied probability (WITH the vig). Null on bad input. */
function americanToImpliedProb(american) {
const a = Number(american);
if (!Number.isFinite(a) || a === 0) return null;
return a > 0 ? 100 / (a + 100) : (-a) / ((-a) + 100);
}
/** American odds → decimal odds (total return multiple incl. stake). */
function americanToDecimal(american) {
const a = Number(american);
if (!Number.isFinite(a) || a === 0) return null;
return a > 0 ? 1 + a / 100 : 1 + 100 / (-a);
}
/** Probability → fair American odds. Null outside (0,1). */
function impliedProbToAmerican(p) {
if (!Number.isFinite(p) || p <= 0 || p >= 1) return null;
// Even money (50%) is +100 by convention → favorites (p > .5) go negative.
return p > 0.5 ? -Math.round((p / (1 - p)) * 100) : Math.round(((1 - p) / p) * 100);
}
/**
* Two-way de-vig. Given BOTH sides' American odds, return fair prob + fair price
* per side plus the overround. Null when a side's odds are missing/invalid —
* the caller marks fair as unavailable rather than inventing it.
*/
function devigTwoWay(overOdds, underOdds) {
const po = americanToImpliedProb(overOdds);
const pu = americanToImpliedProb(underOdds);
if (po == null || pu == null) return null;
const sum = po + pu; // > 1 by the vig
if (!(sum > 0)) return null;
const fairOver = po / sum;
const fairUnder = pu / sum;
return {
method: 'multiplicative',
overround: round3(sum - 1),
over: { fair_prob: round3(fairOver), fair_odds: impliedProbToAmerican(fairOver) },
under: { fair_prob: round3(fairUnder), fair_odds: impliedProbToAmerican(fairUnder) },
};
}
/**
* Expected value (%) of staking one unit on `american` when the model gives the
* side probability `modelProb`. EV per unit = p·decimal 1. Positive = the bet
* is +EV at the price you'd actually pay (the vig is already baked into the
* actual price, so this is the true "does it pay you" measure). Null on bad input.
*/
function evPct(modelProb, american) {
const dec = americanToDecimal(american);
if (!Number.isFinite(modelProb) || modelProb <= 0 || dec == null) return null;
return Math.round((modelProb * dec - 1) * 1000) / 10; // one decimal place
}
module.exports = {
americanToImpliedProb,
americanToDecimal,
impliedProbToAmerican,
devigTwoWay,
evPct,
};