-- 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.

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 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;
