Suppress rare-event 0.5 unders (juiced, no-edge) — config-driven grade + board fix
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>
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
# VYNDR — STATE OF THE WORLD
|
||||
### As of `a8e383e` (main, deployed + fingerprint-verified live), 2026-07-18. This file opens every future session. Update it when a train ships.
|
||||
|
||||
## RARE-EVENT UNDER SUPPRESSION (2026-07-19, on main)
|
||||
Betting-logic audit: the CONSENSUS-vs-MODEL board flooded with "doubles u0.5 · MODEL 0.2 · +edge" fake reads (juiced rare-event unders). Report: the doubles projection is REAL per-player (MLB_LOG_FIELD doubles→doubles; values varied 0.03/0.16/0.2/0.22) — NOT a flat fallback; the issue is purely structural.
|
||||
- **Config-driven** (`src/config/rareEventMarkets.js`): RARE_EVENT_STATS = doubles/triples/home_runs/stolen_bases, RARE_EVENT_LINE_MAX = 0.5.
|
||||
- **Grade layer** (`analyzeViaEngine1`): rare-event UNDER at ≤0.5 → always refused (grade null + suppressed); rare-event OVER at ≤0.5 → refused unless projection > line (a 0.2-over-0.5 carries the SAME |edge| as the under, so it'd just take its board rank — refusing it is what actually clears the market). OVER with a genuine projection > line still grades.
|
||||
- **Board layer** (`marketBreadth.collectBreadth`): drops null-model rows — a suppressed/ungraded prop can't rank a "MODEL —" placeholder onto the board.
|
||||
- Suite 274/3289 green. Fingerprint: next MLB snapshot should have no rare-event u0.5 grades.
|
||||
|
||||
## BACKUP + FOUNDER CHECKOUT (2026-07-18, on main)
|
||||
- **Task A — DB backup (SHIPPED; Kev cron+fingerprint on box)** `c2c43cd`: Dockerfile now has pg_dump/pg_restore/rsync; `backup-db.sh` validates every dump via `pg_restore --list` (must contain ledger_entries). Runs IN the API container (SUPABASE_DB_URL is there; WSL2 can't reach Supabase). Runbook = host cron `docker exec <api> sh /app/scripts/backup-db.sh`. **Mechanism fingerprint PASSED locally** (137 rows → dump → validate → restore → 137 rows). Kev: install the host cron + run the prod restore fingerprint.
|
||||
- **Task B — founder checkout SEAT-GATED (SHIPPED)** `ccb9668`: `resolveCheckoutPrice` attaches the founder price while seats remain (< FOUNDER_SEATS_TOTAL, same `countFounderSeats()` truth as the meter), flips to standard at 100; meter shows SOLD OUT. Fixes "Claim a Founder Desk" charging $44.99 vs advertised $34.99. **payment_failed grace 48h→14d** (spans Stripe retries; revoke only on real cancel). Tested seat 0/99/100/null. Needs `STRIPE_PRICE_*_FOUNDER` set in prod for founder pricing to activate.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Rare-event markets (betting-logic audit, 2026-07-19).
|
||||
*
|
||||
* On a 0.5-line rare counting stat — doubles, triples, home runs, stolen bases —
|
||||
* the UNDER is the juiced side the book wants action on: the event just usually
|
||||
* doesn't happen, so a real projection of (say) 0.2 always "favors" under 0.5,
|
||||
* but there's no takeable edge (the book prices it -250+). Surfacing those
|
||||
* unders floods the board with fake B-grade "under 0.5" reads and violates the
|
||||
* standing no-unders-default doctrine.
|
||||
*
|
||||
* So we SUPPRESS the under side on these markets at/below the line threshold —
|
||||
* it is not graded. The OVER side still grades normally and only surfaces when
|
||||
* the model genuinely projects the event above the line.
|
||||
*
|
||||
* Config-driven (a stat list + a line threshold) so it's tunable without
|
||||
* touching grade logic. CommonJS so it's unit-testable + requireable everywhere.
|
||||
*/
|
||||
|
||||
// Low-frequency counting stats where a 0.5 under is structurally a bad bet.
|
||||
const RARE_EVENT_STATS = ['doubles', 'triples', 'home_runs', 'stolen_bases'];
|
||||
|
||||
// The under is suppressed only at/below this line (0.5 is the juiced rare line;
|
||||
// a 1.5+ line is a different market where an under can be a real read).
|
||||
const RARE_EVENT_LINE_MAX = 0.5;
|
||||
|
||||
function isRareEventStat(statType) {
|
||||
return RARE_EVENT_STATS.includes(String(statType || '').toLowerCase());
|
||||
}
|
||||
|
||||
/** True when this prop is a rare-event UNDER at/below the line threshold —
|
||||
* the juiced side, always refused. Needs no projection. */
|
||||
function isSuppressedRareUnder(statType, line, direction) {
|
||||
const dir = String(direction || '').toLowerCase();
|
||||
const ln = Number(line);
|
||||
return dir === 'under'
|
||||
&& isRareEventStat(statType)
|
||||
&& Number.isFinite(ln)
|
||||
&& ln <= RARE_EVENT_LINE_MAX;
|
||||
}
|
||||
|
||||
/** True when this prop is a rare-event OVER at/below the threshold that the
|
||||
* model does NOT genuinely project (projection <= line). Refusing it is what
|
||||
* keeps the board clean — a 0.2-projection over 0.5 carries the SAME |edge| as
|
||||
* the suppressed under, so if we let it grade it just takes the under's place.
|
||||
* The over is a real read only when the model projects ABOVE the line. */
|
||||
function isSuppressedRareOver(statType, line, direction, projection) {
|
||||
const dir = String(direction || '').toLowerCase();
|
||||
if (dir !== 'over') return false;
|
||||
const ln = Number(line);
|
||||
const proj = Number(projection);
|
||||
return isRareEventStat(statType)
|
||||
&& Number.isFinite(ln)
|
||||
&& ln <= RARE_EVENT_LINE_MAX
|
||||
&& !(Number.isFinite(proj) && proj > ln);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
RARE_EVENT_STATS,
|
||||
RARE_EVENT_LINE_MAX,
|
||||
isRareEventStat,
|
||||
isSuppressedRareUnder,
|
||||
isSuppressedRareOver,
|
||||
};
|
||||
@@ -16,6 +16,7 @@
|
||||
const { computeFeaturesForProp } = require('./computeFeatures');
|
||||
const engine1 = require('./engine1');
|
||||
const { toLegacyShape } = require('../../utils/gradeAdapter');
|
||||
const { isSuppressedRareUnder, isSuppressedRareOver } = require('../../config/rareEventMarkets');
|
||||
|
||||
// Map an error code from computeFeaturesForProp.meta.errors into a human
|
||||
// sentence the user will see in reasoning.summary.
|
||||
@@ -293,6 +294,30 @@ function insufficientDataResult(rawProp, errors) {
|
||||
};
|
||||
}
|
||||
|
||||
// Betting-logic audit (2026-07-19) — rare-event 0.5 markets (doubles/triples/
|
||||
// HR/SB) have no takeable edge on the UNDER (juiced) and no edge on the OVER
|
||||
// unless the model genuinely projects the event above the line. We REFUSE those
|
||||
// (grade null + insufficient_data so every consumer's no-read handling applies)
|
||||
// with a distinct `suppressed` flag/reason.
|
||||
function suppressedRareResult(rawProp, reason, summary) {
|
||||
return {
|
||||
player: rawProp.player ?? null,
|
||||
stat_type: rawProp.stat_type ?? null,
|
||||
line: rawProp.line ?? null,
|
||||
direction: rawProp.direction ?? null,
|
||||
book: rawProp.book || 'unknown',
|
||||
grade: null,
|
||||
insufficient_data: true,
|
||||
suppressed: true,
|
||||
suppressed_reason: reason,
|
||||
confidence: 0,
|
||||
edge_pct: 0,
|
||||
projection: null,
|
||||
kill_conditions_triggered: [],
|
||||
reasoning: { summary, steps: [] },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Form score (0..100) from recent-vs-baseline averages (Session 43). Hot
|
||||
* (l5 > l20) trends above 70; cold below. undefined when there's no recent avg.
|
||||
@@ -359,6 +384,14 @@ function buildIntelFields(features = {}, opts = {}) {
|
||||
}
|
||||
|
||||
async function analyzeViaEngine1(rawProp = {}) {
|
||||
// Betting-logic audit — suppress the juiced UNDER up front (no compute spent):
|
||||
// a 0.5-line under on a rare counting stat (doubles/triples/HR/SB) is never a
|
||||
// takeable edge. (The OVER is gated on the projection below.)
|
||||
if (isSuppressedRareUnder(rawProp.stat_type, rawProp.line, rawProp.direction)) {
|
||||
return suppressedRareResult(rawProp, 'rare_event_under',
|
||||
`No read — a ${rawProp.line} under on ${rawProp.stat_type} is a juiced rare-event market, not a takeable edge.`);
|
||||
}
|
||||
|
||||
const featureResult = await computeFeaturesForProp(rawProp);
|
||||
const { features, trap, consistency, prop, meta } = featureResult;
|
||||
|
||||
@@ -381,6 +414,15 @@ async function analyzeViaEngine1(rawProp = {}) {
|
||||
return insufficientDataResult(rawProp, meta?.errors);
|
||||
}
|
||||
|
||||
// Betting-logic audit — a rare-event 0.5 OVER is a read ONLY when the model
|
||||
// genuinely projects the event ABOVE the line. Below that, the over carries
|
||||
// the same |edge| as the (already-suppressed) under and would just take its
|
||||
// place on the board — so refuse it. This is what actually clears the market.
|
||||
if (isSuppressedRareOver(rawProp.stat_type, prop.line, prop.direction, projection)) {
|
||||
return suppressedRareResult(rawProp, 'rare_event_over_below_line',
|
||||
`No read — the model projects ${projection} ${rawProp.stat_type}, at or below the ${prop.line} line; the over is not a genuine event projection.`);
|
||||
}
|
||||
|
||||
// Engine 1: deterministic rule-based grade on the feature vector.
|
||||
const engine1Result = engine1.gradeProp({ features, trap, consistency, prop });
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
'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
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,8 @@ describe('books lib (BookChip data)', () => {
|
||||
it('maps known books to brand colors (case-insensitive aliases)', () => {
|
||||
expect(bookInfo('DK')).toMatchObject({ name: 'DraftKings', fg: '#53D337' });
|
||||
expect(bookInfo('draftkings').mono).toBe('DK');
|
||||
expect(bookInfo('ESPN').mono).toBe('EB');
|
||||
// ESPN BET retired → theScore Bet (PENN) successor (item 7).
|
||||
expect(bookInfo('THESCORE')).toMatchObject({ name: 'theScore Bet', mono: 'TS' });
|
||||
});
|
||||
it('degrades unknown books to a neutral chip', () => {
|
||||
const b = bookInfo('ZZZ');
|
||||
|
||||
@@ -104,6 +104,11 @@ function collectBreadth(items, limit = 6) {
|
||||
if (!it) continue;
|
||||
const b = computeBreadth(it.books, it.modelValue, it.side);
|
||||
if (!b) continue;
|
||||
// Betting-logic audit — a CONSENSUS-vs-MODEL row needs a model opinion. A
|
||||
// prop with no model (ungraded, or a refused/suppressed read like a
|
||||
// rare-event 0.5 market) has nothing to compare — drop it, don't rank a
|
||||
// "MODEL —" placeholder onto the board.
|
||||
if (b.model == null || b.signedEdge == null) continue;
|
||||
out.push({ player: it.player, stat: it.stat, line: numOrNull(it.line), ...b });
|
||||
}
|
||||
const abs = (x) => Math.abs(numOrNull(x) == null ? 0 : numOrNull(x));
|
||||
|
||||
Reference in New Issue
Block a user