7a925f43eb
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>
56 lines
2.3 KiB
JavaScript
56 lines
2.3 KiB
JavaScript
'use strict';
|
|
|
|
const devig = require('../../src/utils/devig');
|
|
|
|
describe('de-vig engine (step 1)', () => {
|
|
test('american → implied prob (favorite + dog + break-even)', () => {
|
|
expect(devig.americanToImpliedProb(-110)).toBeCloseTo(0.5238, 3);
|
|
expect(devig.americanToImpliedProb(+150)).toBeCloseTo(0.4, 3);
|
|
expect(devig.americanToImpliedProb(+100)).toBeCloseTo(0.5, 3);
|
|
expect(devig.americanToImpliedProb(0)).toBeNull();
|
|
expect(devig.americanToImpliedProb(null)).toBeNull();
|
|
});
|
|
|
|
test('prob → fair american is the inverse', () => {
|
|
expect(devig.impliedProbToAmerican(0.5)).toBe(100); // +100 at 50%
|
|
expect(devig.impliedProbToAmerican(0.6)).toBe(-150);
|
|
expect(devig.impliedProbToAmerican(0.4)).toBe(150);
|
|
expect(devig.impliedProbToAmerican(0)).toBeNull();
|
|
expect(devig.impliedProbToAmerican(1)).toBeNull();
|
|
});
|
|
|
|
test('two-way de-vig strips the vig; fair probs sum to 1', () => {
|
|
// -110 / -110: each implies .5238, sum 1.0476 (4.76% vig) → fair .5 / .5
|
|
const d = devig.devigTwoWay(-110, -110);
|
|
expect(d.method).toBe('multiplicative');
|
|
expect(d.overround).toBeCloseTo(0.048, 2);
|
|
expect(d.over.fair_prob).toBeCloseTo(0.5, 3);
|
|
expect(d.under.fair_prob).toBeCloseTo(0.5, 3);
|
|
expect(d.over.fair_prob + d.under.fair_prob).toBeCloseTo(1, 6);
|
|
});
|
|
|
|
test('a juiced book price de-vigs to a fairer number (the story)', () => {
|
|
// book -145 over vs +115 under
|
|
const d = devig.devigTwoWay(-145, +115);
|
|
expect(d.over.fair_prob + d.under.fair_prob).toBeCloseTo(1, 6);
|
|
// the fair over price is less juiced than the -145 book price
|
|
expect(d.over.fair_odds).toBeGreaterThan(-145);
|
|
});
|
|
|
|
test('only one side priced → fair UNAVAILABLE (never faked)', () => {
|
|
expect(devig.devigTwoWay(-110, null)).toBeNull();
|
|
expect(devig.devigTwoWay(null, -110)).toBeNull();
|
|
expect(devig.devigTwoWay(undefined, undefined)).toBeNull();
|
|
});
|
|
|
|
test('EV% at the actual price from the model probability', () => {
|
|
// model 55% on a +100 line: 0.55*2 - 1 = +10%
|
|
expect(devig.evPct(0.55, +100)).toBeCloseTo(10, 1);
|
|
// model 52.38% on -110 (= break-even): ~0 EV
|
|
expect(devig.evPct(0.5238, -110)).toBeCloseTo(0, 0);
|
|
// model 50% on -110 → negative (you pay the vig)
|
|
expect(devig.evPct(0.5, -110)).toBeLessThan(0);
|
|
expect(devig.evPct(null, -110)).toBeNull();
|
|
});
|
|
});
|