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:
@@ -1,11 +1,11 @@
|
||||
const axios = require('axios');
|
||||
const { getRedisClient } = require('../utils/redis');
|
||||
const { normalizeProps, MARKET_MAP } = require('../utils/oddsNormalizer');
|
||||
const { normalizeProps, extractSpreads, MARKET_MAP } = require('../utils/oddsNormalizer');
|
||||
|
||||
const ODDS_API_BASE = 'https://api.the-odds-api.com/v4/sports';
|
||||
const CACHE_TTL = 900; // 15 minutes in seconds
|
||||
const SPORT_KEYS = { nba: 'basketball_nba', ncaab: 'basketball_ncaab' };
|
||||
const ALL_MARKETS = Object.keys(MARKET_MAP).join(',');
|
||||
const ALL_MARKETS = Object.keys(MARKET_MAP).join(',') + ',spreads';
|
||||
const BOOKMAKERS = 'draftkings,fanduel,betmgm';
|
||||
|
||||
function getCacheKey(sport) {
|
||||
@@ -91,11 +91,12 @@ async function fetchAllOdds(sport, apiKey) {
|
||||
}
|
||||
|
||||
const props = normalizeProps(eventsWithOdds);
|
||||
const spreads = extractSpreads(eventsWithOdds);
|
||||
const quotaRemaining = lastHeaders['x-requests-remaining']
|
||||
? parseInt(lastHeaders['x-requests-remaining'], 10)
|
||||
: null;
|
||||
|
||||
return { props, quotaRemaining, headers: lastHeaders };
|
||||
return { props, spreads, quotaRemaining, headers: lastHeaders };
|
||||
}
|
||||
|
||||
function parseQuota(headers) {
|
||||
@@ -119,6 +120,7 @@ async function getOdds(sport) {
|
||||
source: 'cache',
|
||||
quota_remaining: quota,
|
||||
props: data.props,
|
||||
spreads: data.spreads || [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -132,7 +134,7 @@ async function getOdds(sport) {
|
||||
|
||||
// Fetch live data
|
||||
try {
|
||||
const { props, quotaRemaining, headers } = await fetchAllOdds(sport, apiKey);
|
||||
const { props, spreads, quotaRemaining, headers } = await fetchAllOdds(sport, apiKey);
|
||||
|
||||
// Update quota in Redis
|
||||
if (headers) {
|
||||
@@ -140,7 +142,7 @@ async function getOdds(sport) {
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const cacheData = { updated_at: now, props };
|
||||
const cacheData = { updated_at: now, props, spreads };
|
||||
await redis.set(cacheKey, JSON.stringify(cacheData), 'EX', CACHE_TTL);
|
||||
|
||||
return {
|
||||
@@ -149,6 +151,7 @@ async function getOdds(sport) {
|
||||
source: 'live',
|
||||
quota_remaining: quotaRemaining,
|
||||
props,
|
||||
spreads,
|
||||
};
|
||||
} catch (err) {
|
||||
// If API fails, try stale cache (no TTL check — any cached data)
|
||||
@@ -163,6 +166,7 @@ async function getOdds(sport) {
|
||||
stale: true,
|
||||
quota_remaining: quota,
|
||||
props: data.props,
|
||||
spreads: data.spreads || [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user