Files
vyndr/scripts/tb-compound-holdout.sql
T
builtbykev e29ab6fd6a Takeable enforcement: verified on real rows, 1,006 tagged, re-stamp call ready
PART 1 verified by inducing the REAL rowsFromSnapshot over REAL lock_lines
rows from prod. Three cases, 0 non-takeable anchors:
  Narvaez  (dabble/kalshi/prizepicks/smarkets, NO takeable book)
           -> book=null, price=null, takeable=null  [honest absent]
  Schwarber(bovada/dabble/novig/PINNACLE before draftkings)
           -> draftkings +102  [pinnacle SKIPPED, proving TAKEABLE not MODEL]
  Ohtani   (dabble/onexbet before draftkings) -> draftkings -266
Narvaez is the case that matters: pre-fix he was stamped dabble +104
takeable=true; he is now honestly absent.

A HARNESS BUG RECORDED: my first verification pulled live /api/odds/mlb,
which returned {"error":"Odds data temporarily unavailable"}. The script
read that as 0 props and printed "all from takeable books? true" -- a
VACUOUSLY TRUE pass. I caught it only because I also printed the book list
and it was empty. Same family as the silent-false traps: a probe that finds
nothing looks identical to a probe that finds nothing wrong.

PART 2: 1,006 rows tagged via the purpose-built quarantine_reason at ROW
level with three sub-cases (recoverable_same_line 936, no_takeable_quote
49, takeable_line_differs 21). getModelAggregate ALREADY excluded
quarantined rows, so the public record and the n>=20 gate were clean
automatically; all five committed holdout scripts now carry the exclusion
explicitly.

PART 3 -- the re-stamp call is now fact-based. The takeable LOCK-TIME price
is recoverable for 936/1,006 (93.0%) from lock_lines, the correct
instrument. Only 431 appear in closing_captures, which is the wrong timing
for a lock price anyway.

LINE CONTAMINATION ANSWERED (previously unverified): the stored line
MATCHES a takeable book's line on 936 (93.0%), DIFFERS on 21 (2.1%), and is
unverifiable on 49 (4.9%) where no takeable book quoted the prop at all.

That makes it cleanly row-level: re-stamp the 936 as an honest JOIN and
recover 886 pending rows for the holdouts, or leave all 1,006 excluded.
Either way the 21 + 49 stay out -- re-stamping those would invent a lock
price, or a line, we never captured. Nothing re-stamped; Kev's call.

Gates: 4,111 tests / 330 suites green; next build exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
2026-08-02 14:24:28 -04:00

52 lines
2.5 KiB
SQL

-- tb-compound-holdout.sql — TOTAL BASES ONLY, direction-aligned.
--
-- TWO guards this query exists to enforce:
-- 1. TB ROWS ONLY. Averaging into other stats would hide the effect, since
-- total_bases is 49 of 437 settled rows.
-- 2. DIRECTION-ALIGNED. p_win is P(GRADED SIDE); proj_p_over_line and
-- proj_tb_p_over are P(OVER). 31.4% of rows are under-graded, and comparing
-- raw P(over) against an under-side outcome measures the model BACKWARDS —
-- that artifact alone accounted for 41% of the ladder's apparent loss.
--
-- Promote tb-v1 ONLY if it materially improves TB resolution toward/past the
-- champion. If it does NOT, the family-mismatch hypothesis is WRONG and the
-- mean-weakness / similarity branch REOPENS. Record which.
--
-- CONTAMINATION EXCLUSION (2026-08-02, MANDATORY). Rows whose price/book/takeable
-- were stamped from a NON-TAKEABLE book (DFS / offshore / exchange) between
-- 2026-08-01 and the write-path fix are tagged `quarantine_reason LIKE
-- 'nontakeable_book%'`. They are EXCLUDED here and must never be pooled with
-- clean rows: their locked price -- and therefore the `takeable` flag computed
-- from it -- describes a market you could not have bet.
with tb as (
select
game_date, id, lower(side) side, (outcome='hit')::int won,
p_win::numeric champ,
case when lower(side)='under' then 1 - proj_p_over_line::numeric
else proj_p_over_line::numeric end ladder_al,
case when lower(side)='under' then 1 - proj_tb_p_over::numeric
else proj_tb_p_over::numeric end tbv1_al
from public.ledger_entries
where sport='mlb' and user_id is null
and (quarantine_reason is null or quarantine_reason not like 'nontakeable_book%')
and stat = 'total_bases'
and outcome in ('hit','miss')
and p_win is not null
and proj_p_over_line is not null
and proj_tb_p_over is not null -- matched rows: all three present
)
select
count(*) n,
count(*) filter (where side='under') under_rows,
round(avg(won::numeric),3) base_rate,
round(corr(champ, won::numeric)::numeric,4) res_champion,
round(corr(ladder_al, won::numeric)::numeric,4) res_ladder_v11,
round(corr(tbv1_al, won::numeric)::numeric,4) res_tb_v1,
round(stddev(ladder_al)::numeric,4) sd_ladder,
round(stddev(tbv1_al)::numeric,4) sd_tb_v1,
round(avg(ladder_al)::numeric,4) mean_ladder,
round(avg(tbv1_al)::numeric,4) mean_tb_v1
from tb;