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
@@ -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 });