f72f063e6f
Betting-logic audit: the CONSENSUS-vs-MODEL board flooded with fake reads like "DOUBLES u0.5 · MODEL 0.2 · +edge" — the juiced under side of rare counting-stat markets (doubles/triples/HR/SB), which is never a takeable edge and violates the no-unders-default doctrine. Report finding (item 3/4): the doubles projection is REAL per-player, not a flat fallback — 'doubles' maps to a real game-log field (MLB_LOG_FIELD doubles→ doubles) and the live values varied (0.03/0.16/0.2/0.22). So no projection-gate refusal for fakeness; the problem is purely structural (a rare event's real projection always sits below a 0.5 line, so the under always "wins"). Fix (config-driven — src/config/rareEventMarkets.js, tunable stat list + line threshold): - Grade layer (analyzeViaEngine1): a rare-event UNDER at ≤0.5 is always REFUSED (grade null + suppressed flag/reason). A rare-event OVER at ≤0.5 is refused UNLESS the model genuinely projects the event above the line — because a 0.2-over-0.5 carries the SAME |edge| as the suppressed under and would just take its rank on the board. The over grades normally once projection > line. - Board layer (marketBreadth.collectBreadth): drops null-model rows so a suppressed/ungraded prop can't rank a "MODEL —" placeholder onto the board. 10 suppression tests + config locks; also fixed a settingsPage book assertion left over from the ESPN→theScore swap. Suite 274/3289 green, web build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
5.8 KiB
JavaScript
115 lines
5.8 KiB
JavaScript
'use strict';
|
|
|
|
// Betting-logic audit (2026-07-19) — rare-event 0.5 markets. The UNDER is
|
|
// always suppressed (juiced); the OVER grades only when the model genuinely
|
|
// projects the event above the line; and the CONSENSUS-vs-MODEL board drops
|
|
// no-model rows so a suppressed market can't take the under's rank.
|
|
|
|
// ── config helpers (pure) ───────────────────────────────────────────────────
|
|
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']);
|
|
expect(cfg.RARE_EVENT_LINE_MAX).toBe(0.5);
|
|
});
|
|
|
|
test('UNDER on a rare 0.5 market is suppressed', () => {
|
|
for (const s of cfg.RARE_EVENT_STATS) {
|
|
expect(cfg.isSuppressedRareUnder(s, 0.5, 'under')).toBe(true);
|
|
}
|
|
expect(cfg.isSuppressedRareUnder('doubles', 0.5, 'over')).toBe(false); // over handled separately
|
|
expect(cfg.isSuppressedRareUnder('hits', 0.5, 'under')).toBe(false); // hits is NOT rare
|
|
expect(cfg.isSuppressedRareUnder('doubles', 1.5, 'under')).toBe(false); // 1.5 line is a diff market
|
|
});
|
|
|
|
test('OVER on a rare 0.5 market is suppressed UNLESS projection > line', () => {
|
|
expect(cfg.isSuppressedRareOver('doubles', 0.5, 'over', 0.2)).toBe(true); // 0.2 <= 0.5 → junk
|
|
expect(cfg.isSuppressedRareOver('doubles', 0.5, 'over', 0.5)).toBe(true); // == line → not genuine
|
|
expect(cfg.isSuppressedRareOver('doubles', 0.5, 'over', 0.7)).toBe(false); // 0.7 > 0.5 → genuine read
|
|
expect(cfg.isSuppressedRareOver('doubles', 0.5, 'under', 0.2)).toBe(false); // under path, not this fn
|
|
expect(cfg.isSuppressedRareOver('hits', 0.5, 'over', 0.2)).toBe(false); // not rare
|
|
});
|
|
});
|
|
|
|
// ── analyzeViaEngine1 integration ───────────────────────────────────────────
|
|
const mockComputeReturn = { current: null };
|
|
jest.mock('../../src/services/intelligence/computeFeatures', () => ({
|
|
computeFeaturesForProp: async () => mockComputeReturn.current,
|
|
}));
|
|
const mockEngine1Return = { current: null };
|
|
jest.mock('../../src/services/intelligence/engine1', () => ({
|
|
gradeProp: () => mockEngine1Return.current,
|
|
}));
|
|
const { analyzeViaEngine1 } = require('../../src/services/intelligence/analyzeViaEngine1');
|
|
|
|
const feat = (l5, line, dir) => ({
|
|
features: { l5_avg: l5, l20_avg: l5 },
|
|
trap: {}, consistency: { consistency: 'reliable', score: 0.7 },
|
|
prop: { line, direction: dir }, meta: { sport: 'mlb', errors: [] },
|
|
});
|
|
|
|
beforeEach(() => {
|
|
mockComputeReturn.current = null;
|
|
mockEngine1Return.current = { grade: 'B', confidence: 0.55, top_factors: [], all_factors: [] };
|
|
});
|
|
|
|
describe('analyzeViaEngine1 — rare-event suppression', () => {
|
|
test('doubles UNDER 0.5 is REFUSED (grade null, suppressed) — no compute', async () => {
|
|
const out = await analyzeViaEngine1({ player: 'X', stat_type: 'doubles', line: 0.5, direction: 'under' });
|
|
expect(out.grade).toBeNull();
|
|
expect(out.suppressed).toBe(true);
|
|
expect(out.suppressed_reason).toBe('rare_event_under');
|
|
expect(out.insufficient_data).toBe(true); // filtered by gradeBestSide
|
|
});
|
|
|
|
test('doubles OVER 0.5 with projection 0.2 (<= line) is REFUSED', async () => {
|
|
mockComputeReturn.current = feat(0.2, 0.5, 'over');
|
|
const out = await analyzeViaEngine1({ player: 'X', stat_type: 'doubles', line: 0.5, direction: 'over' });
|
|
expect(out.grade).toBeNull();
|
|
expect(out.suppressed_reason).toBe('rare_event_over_below_line');
|
|
});
|
|
|
|
test('doubles OVER 0.5 with projection 0.7 (> line) GRADES — genuine event read', async () => {
|
|
mockComputeReturn.current = feat(0.7, 0.5, 'over');
|
|
const out = await analyzeViaEngine1({ player: 'X', stat_type: 'doubles', line: 0.5, direction: 'over' });
|
|
expect(out.grade).toBe('B'); // real grade, not suppressed
|
|
expect(out.suppressed).toBeUndefined();
|
|
});
|
|
|
|
test('home_runs / stolen_bases / triples UNDER 0.5 all suppressed', async () => {
|
|
for (const s of ['home_runs', 'stolen_bases', 'triples']) {
|
|
const out = await analyzeViaEngine1({ player: 'X', stat_type: s, line: 0.5, direction: 'under' });
|
|
expect(out.grade).toBeNull();
|
|
expect(out.suppressed).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('a NON-rare under (hits) is unaffected — still grades', async () => {
|
|
mockComputeReturn.current = feat(1.2, 0.5, 'under');
|
|
const out = await analyzeViaEngine1({ player: 'X', stat_type: 'hits', line: 0.5, direction: 'under' });
|
|
expect(out.grade).toBe('B');
|
|
expect(out.suppressed).toBeUndefined();
|
|
});
|
|
|
|
test('a rare stat at a 1.5 line (not 0.5) is unaffected', async () => {
|
|
mockComputeReturn.current = feat(1.2, 1.5, 'under');
|
|
const out = await analyzeViaEngine1({ player: 'X', stat_type: 'home_runs', line: 1.5, direction: 'under' });
|
|
expect(out.grade).toBe('B');
|
|
});
|
|
});
|
|
|
|
// ── board layer: collectBreadth drops no-model rows ─────────────────────────
|
|
const { collectBreadth } = require('../../web/src/lib/marketBreadth');
|
|
|
|
describe('collectBreadth drops null-model rows (suppressed markets off the board)', () => {
|
|
const books = [{ book: 'dk', line: 0.5 }, { book: 'fd', line: 0.5 }];
|
|
test('a prop with no model (refused/ungraded) is not ranked onto the board', () => {
|
|
const rows = collectBreadth([
|
|
{ player: 'Real', stat: 'hits', side: 'over', line: 1.5, books: [{ book: 'dk', line: 1.5 }, { book: 'fd', line: 1.5 }], modelValue: 2.1 },
|
|
{ player: 'Suppressed', stat: 'doubles', side: 'over', line: 0.5, books, modelValue: null },
|
|
], 6);
|
|
expect(rows.map((r) => r.player)).toEqual(['Real']); // the null-model doubles is gone
|
|
});
|
|
});
|