Persist lock-time multi-book lines to lock_lines (unblocks the staleness audit)
The over-side skew audit's confirming check — was our locked line stale-high vs consensus AT LOCK — was BLOCKED because multi-book lines at lock were never persisted (bookprices is Redis current-only). This persists them. - migration 033: lock_lines table (tracked + applied to prod). One row per (graded prop × book) with both odds + a lock timestamp. RLS enabled, NO policies -> service-role only (fence). UNIQUE key -> idempotent re-runs. - lockLineCapture.js: buildLockRows (pure, graded-props only, honest-absent single-book) + idempotent upsert persist. Built from the in-memory props at the LOCK moment (ts) -> no Redis re-read, no TTL race. - snapshotService: persist right after `enriched` (the lock moment; gradedAt uses the same ts). Best-effort + fenced. FENCE (measurement-only): lock_lines is read by NOTHING on the grade path (gradeSlateService, snapshot dedup/indexOdds, challengers, selector, ledger) — a grep test asserts it, and RLS locks it to the service role. Grade byte- identical proven: runSnapshot grades are identical with persist on/off (test). Volume ~1.5-3k rows/day (graded props x books x 5 snapshots); weeks retained, no pruning needed short-term. Does NOT retroactively fix the existing 62 rows — future accrual only; confirmation still needs weeks of settled rows. Full suite 3842 green, web build exit 0. No grade/locked_odds/outcome/served surface changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsztNChZ7vEvSR61AuMhD1
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
-- Migration 033: lock_lines — multi-book lines at the LOCK moment (measurement-only).
|
||||
--
|
||||
-- The lock-time analog of `closing_captures`. As a grade LOCKS to a line, this persists
|
||||
-- each book's line + both-side prices for that (graded) prop, timestamped at the lock
|
||||
-- moment. A future audit joins lock_lines (lock) to closing_captures (close) to answer
|
||||
-- the currently-BLOCKED question: was our locked line stale-high vs consensus AT LOCK?
|
||||
--
|
||||
-- FENCE: measurement/display-only. It must NOT feed the consensus-line selector, the
|
||||
-- champion, any challenger, the graded line, or the ledger. Enforced by (a) a separate
|
||||
-- table nothing on the grade path reads, and (b) RLS enabled with NO policies, so only
|
||||
-- the service role (which bypasses RLS) can read/write it — no client, no anon.
|
||||
--
|
||||
-- One row per (prop × book) carrying BOTH over_odds + under_odds (the de-vig needs both;
|
||||
-- one row halves volume vs closing_captures' per-side rows). Append-only; the UNIQUE key
|
||||
-- makes a re-run (snapshot retry) idempotent. Honest-absent: single-book props persist
|
||||
-- as one row — never a fabricated second book.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS lock_lines (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
sport text NOT NULL,
|
||||
player_key text NOT NULL,
|
||||
player_name text,
|
||||
stat text NOT NULL,
|
||||
game_date date,
|
||||
game_time timestamptz,
|
||||
book text,
|
||||
line_type text, -- 'sharp' (pinnacle) | 'book'
|
||||
line numeric,
|
||||
over_odds integer,
|
||||
under_odds integer,
|
||||
locked_at timestamptz NOT NULL, -- the LOCK moment (snapshot gradedAt ts)
|
||||
missed_reason text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT lock_lines_uniq UNIQUE NULLS NOT DISTINCT (sport, player_key, stat, game_date, book, locked_at)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS lock_lines_join_idx ON lock_lines (sport, player_key, stat, game_date);
|
||||
CREATE INDEX IF NOT EXISTS lock_lines_locked_at_idx ON lock_lines (locked_at);
|
||||
|
||||
-- FENCE: RLS on, no policies → service-role only. Never reachable by a client/anon,
|
||||
-- and never by the grade path (which does not query this table).
|
||||
ALTER TABLE lock_lines ENABLE ROW LEVEL SECURITY;
|
||||
Reference in New Issue
Block a user