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,81 @@
|
||||
-- Feature 2.2 — Line Movement + Cascade Detection
|
||||
-- New tables: line_baselines, line_movements, cascade_alerts
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: line_baselines
|
||||
-- Morning baseline lines for movement detection
|
||||
-- ============================================================
|
||||
CREATE TABLE public.line_baselines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sport TEXT NOT NULL,
|
||||
game_date DATE NOT NULL,
|
||||
player TEXT NOT NULL,
|
||||
stat_type TEXT NOT NULL,
|
||||
book TEXT NOT NULL,
|
||||
baseline_line NUMERIC(5,1) NOT NULL,
|
||||
captured_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_baseline_unique ON public.line_baselines(game_date, player, stat_type, book);
|
||||
CREATE INDEX idx_baseline_date ON public.line_baselines(game_date);
|
||||
|
||||
-- No RLS needed — server-only table, not user-facing
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: line_movements
|
||||
-- Detected line movements throughout the day
|
||||
-- ============================================================
|
||||
CREATE TABLE public.line_movements (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
sport TEXT NOT NULL,
|
||||
game_date DATE NOT NULL,
|
||||
player TEXT NOT NULL,
|
||||
stat_type TEXT NOT NULL,
|
||||
book TEXT NOT NULL,
|
||||
baseline_line NUMERIC(5,1) NOT NULL,
|
||||
current_line NUMERIC(5,1) NOT NULL,
|
||||
movement NUMERIC(4,1) NOT NULL,
|
||||
direction TEXT NOT NULL CHECK (direction IN ('up', 'down')),
|
||||
sharp_indicator TEXT CHECK (sharp_indicator IN ('sharp_action', 'public_action', 'unknown')),
|
||||
detected_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_movements_date ON public.line_movements(game_date);
|
||||
CREATE INDEX idx_movements_player ON public.line_movements(player);
|
||||
|
||||
-- No RLS needed — server-only table
|
||||
|
||||
-- ============================================================
|
||||
-- TABLE: cascade_alerts
|
||||
-- Alerts for users whose scans are affected
|
||||
-- ============================================================
|
||||
CREATE TABLE public.cascade_alerts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||
alert_type TEXT NOT NULL CHECK (alert_type IN ('player_scratched', 'line_moved', 'parlay_regraded')),
|
||||
scan_session_id UUID REFERENCES public.scan_sessions(id),
|
||||
player TEXT,
|
||||
detail TEXT NOT NULL,
|
||||
is_read BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_alerts_user ON public.cascade_alerts(user_id, is_read);
|
||||
|
||||
ALTER TABLE public.cascade_alerts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "alerts_select_own" ON public.cascade_alerts
|
||||
FOR SELECT USING (auth.uid() = user_id);
|
||||
|
||||
CREATE POLICY "alerts_update_own" ON public.cascade_alerts
|
||||
FOR UPDATE USING (auth.uid() = user_id)
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- Cleanup: delete baselines older than 2 days (call periodically)
|
||||
CREATE OR REPLACE FUNCTION public.cleanup_old_baselines()
|
||||
RETURNS void AS $$
|
||||
BEGIN
|
||||
DELETE FROM public.line_baselines WHERE game_date < (CURRENT_DATE - INTERVAL '2 days');
|
||||
DELETE FROM public.line_movements WHERE game_date < (CURRENT_DATE - INTERVAL '2 days');
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
Reference in New Issue
Block a user