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:
Kev
2026-07-19 00:19:23 -04:00
parent b8b954bb96
commit f72f063e6f
6 changed files with 235 additions and 1 deletions
+7
View File
@@ -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.
+65
View File
@@ -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 });
+114
View File
@@ -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
});
});
+2 -1
View File
@@ -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');
+5
View File
@@ -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));