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
+17
View File
@@ -145,6 +145,21 @@ async function getOdds(sport) {
const cacheData = { updated_at: now, props, spreads };
await redis.set(cacheKey, JSON.stringify(cacheData), 'EX', CACHE_TTL);
// Line movement + cascade detection (best-effort, don't block response)
let movements = [];
let scratchedPlayers = [];
try {
const lineMovement = require('./lineMovementService');
const cascade = require('./cascadeService');
const moveResult = await lineMovement.processNewOdds(sport, props);
movements = moveResult.movements || [];
const cascadeResult = await cascade.detectScratches(sport, props);
scratchedPlayers = cascadeResult.scratchedPlayers || [];
} catch (e) {
// Non-fatal — log and continue
console.warn('[BetonBLK] Movement/cascade detection error:', e.message);
}
return {
sport,
updated_at: now,
@@ -152,6 +167,8 @@ async function getOdds(sport) {
quota_remaining: quotaRemaining,
props,
spreads,
movements,
scratchedPlayers,
};
} catch (err) {
// If API fails, try stale cache (no TTL check — any cached data)