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:
@@ -0,0 +1,79 @@
|
||||
const express = require('express');
|
||||
const { analyzeProp } = require('../services/propAnalyzer');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const VALID_STAT_TYPES = new Set([
|
||||
'points', 'rebounds', 'assists', 'threes', 'blocks',
|
||||
'steals', 'pra', 'turnovers',
|
||||
]);
|
||||
|
||||
const VALID_DIRECTIONS = new Set(['over', 'under']);
|
||||
|
||||
function validateProp(prop) {
|
||||
const errors = [];
|
||||
if (!prop.player) errors.push('player is required');
|
||||
if (!prop.stat_type) errors.push('stat_type is required');
|
||||
if (prop.stat_type && !VALID_STAT_TYPES.has(prop.stat_type)) {
|
||||
errors.push(`Invalid stat_type: ${prop.stat_type}`);
|
||||
}
|
||||
if (prop.line == null) errors.push('line is required');
|
||||
if (!prop.direction) errors.push('direction is required');
|
||||
if (prop.direction && !VALID_DIRECTIONS.has(prop.direction)) {
|
||||
errors.push(`Invalid direction: ${prop.direction}`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
router.post('/prop', async (req, res) => {
|
||||
const errors = validateProp(req.body);
|
||||
if (errors.length > 0) {
|
||||
return res.status(400).json({ error: errors.join('; ') });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await analyzeProp(req.body);
|
||||
return res.json(result);
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 404) {
|
||||
return res.status(404).json({ error: `Player not found: ${req.body.player}` });
|
||||
}
|
||||
if (err.statusCode === 429 || err.statusCode === 503) {
|
||||
return res.status(err.statusCode).json({ error: err.message });
|
||||
}
|
||||
console.error('[BetonBLK] Analysis error:', err.message);
|
||||
return res.status(503).json({ error: 'Analysis service temporarily unavailable' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/batch', async (req, res) => {
|
||||
const { props } = req.body;
|
||||
if (!Array.isArray(props) || props.length === 0) {
|
||||
return res.status(400).json({ error: 'props array is required and must not be empty' });
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const prop of props) {
|
||||
const errors = validateProp(prop);
|
||||
if (errors.length > 0) {
|
||||
results.push({ error: errors.join('; '), input: prop });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await analyzeProp(prop);
|
||||
results.push(result);
|
||||
} catch (err) {
|
||||
results.push({
|
||||
error: err.response?.status === 404
|
||||
? `Player not found: ${prop.player}`
|
||||
: 'Analysis failed for this prop',
|
||||
input: prop,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({ results });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user