feat: Feature 1.3 — Prop Analysis Engine with 6-step grading pipeline
Core intelligence for BetonBLK prop analysis: - POST /api/analyze/prop — single prop analysis - POST /api/analyze/batch — multi-prop analysis for parlay scanner - 6-step pipeline: season avg → recent form → situational splits → cross-book lines → kill conditions → grade (A/B/C/D) - 6 kill conditions: low_minutes, small_sample, b2b_high_usage, blowout_risk, split_conflict, no_opponent_data - Composite scoring with confidence (30-95), bonuses, penalties - Added spreads market to Odds API fetch (zero extra credits) - Full reasoning output with step-by-step breakdown 36 new tests (unit + integration), 128 total across all features Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,4 +75,44 @@ function normalizeProps(eventsWithOdds) {
|
||||
return props;
|
||||
}
|
||||
|
||||
module.exports = { normalizeProps, MARKET_MAP, ALLOWED_BOOKS };
|
||||
function extractSpreads(eventsWithOdds) {
|
||||
const spreads = [];
|
||||
|
||||
for (const event of eventsWithOdds) {
|
||||
const homeTeam = getAbbreviation(event.home_team);
|
||||
const awayTeam = getAbbreviation(event.away_team);
|
||||
const gameTime = event.commence_time;
|
||||
|
||||
if (!Array.isArray(event.bookmakers)) continue;
|
||||
|
||||
for (const bookmaker of event.bookmakers) {
|
||||
if (!ALLOWED_BOOKS.has(bookmaker.key)) continue;
|
||||
if (!Array.isArray(bookmaker.markets)) continue;
|
||||
|
||||
for (const market of bookmaker.markets) {
|
||||
if (market.key !== 'spreads') continue;
|
||||
const outcomes = market.outcomes || [];
|
||||
|
||||
for (const outcome of outcomes) {
|
||||
if (outcome.point == null) continue;
|
||||
// Home team spread: outcome.name matches the team full name
|
||||
if (outcome.name === event.home_team) {
|
||||
spreads.push({
|
||||
home_team: homeTeam,
|
||||
away_team: awayTeam,
|
||||
game_time: gameTime,
|
||||
book: bookmaker.key,
|
||||
home_spread: outcome.point,
|
||||
fetched_at: market.last_update,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return spreads;
|
||||
}
|
||||
|
||||
module.exports = { normalizeProps, extractSpreads, MARKET_MAP, ALLOWED_BOOKS };
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
function deltaToSignal(delta) {
|
||||
const abs = Math.abs(delta);
|
||||
if (abs < 0.5) return delta >= 0 ? 'neutral' : 'neutral';
|
||||
if (abs < 2.0) return delta >= 0 ? 'lean' : 'lean_bearish';
|
||||
if (abs < 4.0) return delta >= 0 ? 'bullish' : 'bearish';
|
||||
return delta >= 0 ? 'strong_bullish' : 'strong_bearish';
|
||||
}
|
||||
|
||||
function directedDelta(avg, line, direction) {
|
||||
// For "over", positive delta is good. For "under", negative delta is good.
|
||||
const raw = avg - line;
|
||||
return direction === 'under' ? -raw : raw;
|
||||
}
|
||||
|
||||
module.exports = { deltaToSignal, directedDelta };
|
||||
Reference in New Issue
Block a user