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:
@@ -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;
|
||||
@@ -0,0 +1,27 @@
|
||||
const express = require('express');
|
||||
const { getMovementsForToday } = require('../services/lineMovementService');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const filters = {
|
||||
player: req.query.player || null,
|
||||
stat_type: req.query.stat_type || null,
|
||||
min_movement: req.query.min_movement ? parseFloat(req.query.min_movement) : 0.5,
|
||||
};
|
||||
|
||||
const movements = await getMovementsForToday('nba', filters);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
return res.json({
|
||||
game_date: today,
|
||||
movements,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[BetonBLK] Movements error:', err.message);
|
||||
return res.status(503).json({ error: 'Movement data temporarily unavailable' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+8
-2
@@ -89,13 +89,19 @@ router.get('/nba', async (req, res) => {
|
||||
res.set('X-BetonBLK-Stale', 'true');
|
||||
}
|
||||
|
||||
return res.json({
|
||||
const response = {
|
||||
sport: 'nba',
|
||||
updated_at: result.updated_at,
|
||||
source: result.source,
|
||||
quota_remaining: result.quota_remaining,
|
||||
props,
|
||||
});
|
||||
};
|
||||
|
||||
if (result.movements && result.movements.length > 0) {
|
||||
response.movements = result.movements;
|
||||
}
|
||||
|
||||
return res.json(response);
|
||||
} catch (err) {
|
||||
const status = err.statusCode || 500;
|
||||
return res.status(status).json({ error: err.message });
|
||||
|
||||
Reference in New Issue
Block a user