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