-- opportunity-axis-holdout.sql — the Step 4 proof, RUN WHEN n IS ADEQUATE. -- -- Cannot run yet, by construction: the axis went live 2026-08-01 and its first -- rows carry game_date 2026-08-01 (games not yet played). Settled rows carrying -- the axis: 0. The earliest possible run is the next morning settle pass, and a -- defensible n is several days out at ~140 opportunity-axis rows per snapshot. -- -- BOTH reliability AND resolution must improve for the axis to promote. One or -- neither => SHELVE and record why. -- -- reliability = n-weighted mean |predicted - actual| across deciles. Bucket -- FIRST: mean|p - outcome| on 0/1 rows is noise-dominated individual error, not -- calibration. -- -- 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 rows_ as ( select game_date, id, p_win::numeric champ, p_win_challenger::numeric chal, (outcome='hit')::int won 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 outcome in ('hit','miss') and p_win is not null and p_win_challenger is not null -- ONLY rows the opportunity axis actually touched. Including untouched rows -- would dilute the comparison with rows where challenger === champion by -- construction, and make a null result look like a small positive one. and challenger_adjustments::text like '%opportunity%' ), split as ( select *, case when ntile(2) over (order by game_date, id) = 1 then 'train' else 'holdout' end split from rows_ ), b_champ as ( select split, width_bucket(champ, 0.0, 1.0, 10) bkt, count(*) n, avg(champ) pred, avg(won::numeric) actual from split group by 1,2), b_chal as ( select split, width_bucket(chal, 0.0, 1.0, 10) bkt, count(*) n, avg(chal) pred, avg(won::numeric) actual from split group by 1,2) select s.split, count(*) n, round((select sum(n*abs(pred-actual))/nullif(sum(n),0) from b_champ c where c.split=s.split),4) reliability_champion, round((select sum(n*abs(pred-actual))/nullif(sum(n),0) from b_chal c where c.split=s.split),4) reliability_challenger, round(corr(s.champ, s.won::numeric)::numeric,4) resolution_champion, round(corr(s.chal, s.won::numeric)::numeric,4) resolution_challenger, round(avg(s.won::numeric),4) base_rate from split s group by s.split order by s.split desc;