feat: Feature 2.2 — Line Movement + Cascade Detection

Line movement system:
- Baseline capture on first odds fetch of the day
- Movement detection >= 0.5 points with direction (up/down)
- Sharp money heuristic (sharp_action/public_action/unknown)
- GET /api/movements with player, stat_type, min_movement filters
- Movements included in GET /api/odds/nba live responses

Cascade detection system:
- Scratch detection: player props disappear from 2+ books
- Affected user lookup via scan_sessions + picks
- Parlay re-grade without scratched legs
- cascade_alerts created for affected users
- GET /api/alerts (Analyst/Desk only), PATCH /api/alerts/:id/read

Zero extra Odds API credits — all detection piggybacks on existing fetches.
Migration 002: line_baselines, line_movements, cascade_alerts tables.

30 new tests, 188 total (161 Node.js + 27 Python), all passing.
Phase 2 Core Product COMPLETE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-03-21 14:21:34 -04:00
parent 411cb6f196
commit 2366660f5e
16 changed files with 1450 additions and 41 deletions
+40
View File
@@ -0,0 +1,40 @@
const express = require('express');
const { requireAuth } = require('../middleware/auth');
const { getAlertsForUser, markAlertRead } = require('../services/alertService');
const router = express.Router();
const PAID_TIERS = new Set(['analyst', 'desk']);
router.get('/', requireAuth, async (req, res) => {
if (!PAID_TIERS.has(req.user.tier)) {
return res.status(403).json({ error: 'Alerts are available on Analyst and Desk tiers' });
}
try {
const result = await getAlertsForUser(req.user.id);
return res.json(result);
} catch (err) {
console.error('[BetonBLK] Alerts error:', err.message);
return res.status(503).json({ error: 'Alerts temporarily unavailable' });
}
});
router.patch('/:id/read', requireAuth, async (req, res) => {
if (!PAID_TIERS.has(req.user.tier)) {
return res.status(403).json({ error: 'Alerts are available on Analyst and Desk tiers' });
}
try {
const result = await markAlertRead(req.params.id, req.user.id);
if (!result) {
return res.status(404).json({ error: 'Alert not found' });
}
return res.json(result);
} catch (err) {
console.error('[BetonBLK] Alert update error:', err.message);
return res.status(503).json({ error: 'Alert update failed' });
}
});
module.exports = router;