Generalize the no-edge guard: suppress by the BOOK'S PRICE, not a stat whitelist

Follow-up to the rare-event under fix — the whitelist (doubles/triples/HR/SB)
was fragile: the same juiced-under problem exists for steals, blocks, and any
other low-frequency market, and a new stat would slip through.

The real signal is the book's own price. The doubles unders were priced -625 to
-1100 — laying 6-11x to win 1x on an ~82% event, with no value the model could
recover. So the PRIMARY guard is now stat/sport-agnostic: analyzeViaEngine1
refuses any read whose graded-side odds are past the juice floor
(JUICE_ODDS_FLOOR, default -400, env-tunable). That catches every version of
this — steals, blocks, anything — and it also keeps the public record honest
(those -800 "wins" hit ~82% of the time and would inflate the hit rate, the same
class as the projection-0 degradation).

The structural rare-event rules stay as the BACKUP for props with no odds
(list also expanded cross-sport: + steals, blocks). Normal + longshot prices
(-110, -250, +600) are preserved. 16 tests cover both layers.

Reported: the doubles projection was REAL per-player (not a fallback); the fix
is the price guard, not a bigger list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-19 02:09:11 -04:00
parent f72f063e6f
commit 348a82b4a0
3 changed files with 99 additions and 8 deletions
+44 -2
View File
@@ -9,9 +9,12 @@
const cfg = require('../../src/config/rareEventMarkets');
describe('rareEventMarkets config', () => {
test('the rare stats + threshold are config-driven (tunable)', () => {
expect(cfg.RARE_EVENT_STATS).toEqual(['doubles', 'triples', 'home_runs', 'stolen_bases']);
test('the rare stats + threshold are config-driven (tunable, cross-sport)', () => {
expect(cfg.RARE_EVENT_STATS).toEqual(
expect.arrayContaining(['doubles', 'triples', 'home_runs', 'stolen_bases', 'steals', 'blocks']),
);
expect(cfg.RARE_EVENT_LINE_MAX).toBe(0.5);
expect(cfg.JUICE_ODDS_FLOOR).toBe(-400); // the general guard's default
});
test('UNDER on a rare 0.5 market is suppressed', () => {
@@ -32,6 +35,32 @@ describe('rareEventMarkets config', () => {
});
});
describe('juice floor — the GENERAL, stat-agnostic guard', () => {
test('graded-side odds are read from the graded direction', () => {
expect(cfg.gradedSideOdds({ direction: 'under', under_odds: -800, over_odds: 120 })).toBe(-800);
expect(cfg.gradedSideOdds({ direction: 'over', under_odds: -800, over_odds: 120 })).toBe(120);
expect(cfg.parseAmerican('+600')).toBe(600);
expect(cfg.parseAmerican(null)).toBeNull();
});
test('a side priced past the floor is too juiced (any stat)', () => {
expect(cfg.isTooJuiced({ direction: 'under', under_odds: -800 })).toBe(true); // the doubles case
expect(cfg.isTooJuiced({ direction: 'under', under_odds: -1100 })).toBe(true);
expect(cfg.isTooJuiced({ direction: 'under', under_odds: -400 })).toBe(true); // == floor
});
test('normal + longshot prices are NOT juiced (legit markets preserved)', () => {
expect(cfg.isTooJuiced({ direction: 'under', under_odds: -110 })).toBe(false);
expect(cfg.isTooJuiced({ direction: 'under', under_odds: -250 })).toBe(false);
expect(cfg.isTooJuiced({ direction: 'over', over_odds: 600 })).toBe(false); // a real longshot over
});
test('no odds → cannot judge from price (falls back to the structural rule)', () => {
expect(cfg.isTooJuiced({ direction: 'under' })).toBe(false);
expect(cfg.isTooJuiced({ direction: 'under', under_odds: null })).toBe(false);
});
});
// ── analyzeViaEngine1 integration ───────────────────────────────────────────
const mockComputeReturn = { current: null };
jest.mock('../../src/services/intelligence/computeFeatures', () => ({
@@ -97,6 +126,19 @@ describe('analyzeViaEngine1 — rare-event suppression', () => {
const out = await analyzeViaEngine1({ player: 'X', stat_type: 'home_runs', line: 1.5, direction: 'under' });
expect(out.grade).toBe('B');
});
test('JUICE GUARD — a heavily-juiced side is refused for ANY stat, via price', async () => {
// hits is NOT a rare-event stat, but the book pricing it -800 means no edge.
const out = await analyzeViaEngine1({ player: 'X', stat_type: 'hits', line: 1.5, direction: 'under', under_odds: -800 });
expect(out.grade).toBeNull();
expect(out.suppressed_reason).toBe('juiced_no_edge');
});
test('JUICE GUARD — a normally-priced play still grades', async () => {
mockComputeReturn.current = feat(1.4, 0.5, 'over');
const out = await analyzeViaEngine1({ player: 'X', stat_type: 'hits', line: 0.5, direction: 'over', over_odds: -130 });
expect(out.grade).toBe('B'); // -130 is fine → real read
});
});
// ── board layer: collectBreadth drops no-model rows ─────────────────────────