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:
@@ -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 });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user