Generalize the no-edge guard: suppress by the BOOK'S PRICE, not a stat whitelist

Follow-up to the rare-event under fix — the whitelist (doubles/triples/HR/SB)
was fragile: the same juiced-under problem exists for steals, blocks, and any
other low-frequency market, and a new stat would slip through.

The real signal is the book's own price. The doubles unders were priced -625 to
-1100 — laying 6-11x to win 1x on an ~82% event, with no value the model could
recover. So the PRIMARY guard is now stat/sport-agnostic: analyzeViaEngine1
refuses any read whose graded-side odds are past the juice floor
(JUICE_ODDS_FLOOR, default -400, env-tunable). That catches every version of
this — steals, blocks, anything — and it also keeps the public record honest
(those -800 "wins" hit ~82% of the time and would inflate the hit rate, the same
class as the projection-0 degradation).

The structural rare-event rules stay as the BACKUP for props with no odds
(list also expanded cross-sport: + steals, blocks). Normal + longshot prices
(-110, -250, +600) are preserved. 16 tests cover both layers.

Reported: the doubles projection was REAL per-player (not a fallback); the fix
is the price guard, not a bigger list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-19 02:09:11 -04:00
parent f72f063e6f
commit 348a82b4a0
3 changed files with 99 additions and 8 deletions
+42 -2
View File
@@ -18,8 +18,13 @@
* 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'];
// Low-frequency counting stats (across sports) where a 0.5 under is
// structurally a bad bet. This list is only the BACKUP for props with no odds —
// the primary, stat-agnostic guard is the juice floor below.
const RARE_EVENT_STATS = [
'doubles', 'triples', 'home_runs', 'stolen_bases', // MLB
'steals', 'blocks', // NBA / WNBA
];
// 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).
@@ -56,10 +61,45 @@ function isSuppressedRareOver(statType, line, direction, projection) {
&& !(Number.isFinite(proj) && proj > ln);
}
// ── The juice floor (the GENERAL, stat-agnostic rule) ───────────────────────
// The book's own price is the ground truth of "no takeable edge": a side priced
// worse than this is a heavy favorite where the vig has already eaten any edge,
// regardless of stat or sport. The rare-event doubles unders that started this
// were priced -625 to -1100 — laying 6-11x to win 1x on an ~82% event, with no
// value the model could recover. This catches EVERY version of the problem
// (steals, blocks, any market) without a per-stat whitelist. Tunable via env.
const JUICE_ODDS_FLOOR = Number(process.env.JUICE_ODDS_FLOOR || -400);
function parseAmerican(odds) {
if (odds == null) return null;
const n = Number(String(odds).replace('+', ''));
return Number.isFinite(n) ? n : null;
}
/** American odds on the SIDE being graded (under_odds for under, else over). */
function gradedSideOdds(prop) {
const dir = String((prop && prop.direction) || '').toLowerCase();
const raw = dir === 'under'
? (prop && (prop.under_odds ?? prop.under)) ?? null
: (prop && (prop.over_odds ?? prop.over)) ?? null;
return parseAmerican(raw);
}
/** True when the graded side is too juiced to carry a takeable edge. Odds
* absent → false (can't judge from price; the rare-event rule is the backup). */
function isTooJuiced(prop) {
const o = gradedSideOdds(prop);
return o != null && o <= JUICE_ODDS_FLOOR;
}
module.exports = {
RARE_EVENT_STATS,
RARE_EVENT_LINE_MAX,
isRareEventStat,
isSuppressedRareUnder,
isSuppressedRareOver,
JUICE_ODDS_FLOOR,
parseAmerican,
gradedSideOdds,
isTooJuiced,
};
+13 -4
View File
@@ -16,7 +16,7 @@
const { computeFeaturesForProp } = require('./computeFeatures');
const engine1 = require('./engine1');
const { toLegacyShape } = require('../../utils/gradeAdapter');
const { isSuppressedRareUnder, isSuppressedRareOver } = require('../../config/rareEventMarkets');
const { isSuppressedRareUnder, isSuppressedRareOver, isTooJuiced, gradedSideOdds } = require('../../config/rareEventMarkets');
// Map an error code from computeFeaturesForProp.meta.errors into a human
// sentence the user will see in reasoning.summary.
@@ -384,9 +384,18 @@ 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.)
// Betting-logic audit — the GENERAL no-edge guard (stat/sport-agnostic): the
// book's own price is the truth. A side priced past the juice floor (e.g.
// those doubles unders at -625 to -1100) has no takeable edge, so refuse it up
// front. This is what makes the fix robust instead of a fragile whitelist.
if (isTooJuiced(rawProp)) {
return suppressedRareResult(rawProp, 'juiced_no_edge',
`No read — the book prices this side at ${gradedSideOdds(rawProp)}; the vig has eaten any edge.`);
}
// Backup for props with NO odds — the structural rare-event rule. Suppress the
// juiced UNDER on a 0.5-line rare counting stat (doubles/triples/HR/SB/steals/
// blocks). (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.`);