const { getSupabaseServiceClient } = require('../utils/supabase'); const { getRedisClient } = require('../utils/redis'); function getToday() { return new Date().toISOString().split('T')[0]; } async function processNewOdds(sport, props) { const supabase = getSupabaseServiceClient(); const redis = getRedisClient(); const today = getToday(); const baselineKey = `odds:baseline_set:${sport}:${today}`; // Check if baseline exists for today const hasBaseline = await redis.get(baselineKey); if (!hasBaseline) { // First fetch of the day — capture baseline await captureBaseline(supabase, sport, today, props); await redis.set(baselineKey, '1', 'EX', 86400); return { movements: [], isBaselineCapture: true }; } // Compare current lines to baseline const movements = await detectMovements(supabase, sport, today, props); return { movements, isBaselineCapture: false }; } async function captureBaseline(supabase, sport, today, props) { const rows = props.map((p) => ({ sport, game_date: today, player: p.player, stat_type: p.stat_type, book: p.book, baseline_line: p.line, })); if (rows.length === 0) return; // Upsert to handle re-captures (unique index prevents duplicates) await supabase.from('line_baselines').upsert(rows, { onConflict: 'game_date,player,stat_type,book', }); // Cleanup old baselines await supabase.from('line_baselines').delete().lt('game_date', getOldDate()); await supabase.from('line_movements').delete().lt('game_date', getOldDate()); } async function detectMovements(supabase, sport, today, props) { // Fetch today's baselines const { data: baselines } = await supabase .from('line_baselines') .select('*') .eq('game_date', today); if (!baselines || baselines.length === 0) return []; // Build lookup const baselineMap = {}; for (const b of baselines) { baselineMap[`${b.player}::${b.stat_type}::${b.book}`] = b; } const movements = []; for (const prop of props) { const key = `${prop.player}::${prop.stat_type}::${prop.book}`; const baseline = baselineMap[key]; if (!baseline) continue; const diff = prop.line - baseline.baseline_line; if (Math.abs(diff) < 0.5) continue; const direction = diff > 0 ? 'up' : 'down'; const sharpIndicator = determineSharpIndicator( baseline.baseline_line, prop.line, prop.over_odds, prop.under_odds, direction ); const movement = { sport, game_date: today, player: prop.player, stat_type: prop.stat_type, book: prop.book, baseline_line: baseline.baseline_line, current_line: prop.line, movement: Math.round(Math.abs(diff) * 10) / 10, direction, sharp_indicator: sharpIndicator, }; movements.push(movement); } // Store movements in DB if (movements.length > 0) { await supabase.from('line_movements').insert(movements); } return movements; } function determineSharpIndicator(baselineLine, currentLine, overOdds, underOdds, direction) { // Heuristic: if line moves up (higher) and over odds get worse (more negative), // sharps are on the under. If line moves down and under odds get worse, sharps on over. if (overOdds == null || underOdds == null) return 'unknown'; if (direction === 'up') { // Line went up — if over is expensive (< -115), sharps may be on under if (overOdds < -115) return 'sharp_action'; if (underOdds < -115) return 'public_action'; } else { // Line went down — if under is expensive, sharps may be on over if (underOdds < -115) return 'sharp_action'; if (overOdds < -115) return 'public_action'; } return 'unknown'; } function getOldDate() { const d = new Date(); d.setUTCDate(d.getUTCDate() - 2); return d.toISOString().split('T')[0]; } async function getMovementsForToday(sport, filters = {}) { const supabase = getSupabaseServiceClient(); const today = getToday(); let query = supabase .from('line_movements') .select('*') .eq('game_date', today); if (filters.player) { query = query.ilike('player', `%${filters.player}%`); } if (filters.stat_type) { query = query.eq('stat_type', filters.stat_type); } const { data: movements } = await query; let filtered = movements || []; if (filters.min_movement) { filtered = filtered.filter((m) => m.movement >= filters.min_movement); } return filtered; } module.exports = { processNewOdds, captureBaseline, detectMovements, determineSharpIndicator, getMovementsForToday, };