-- pwin-timeforward.sql — calibration refresh on the CURRENT MLB sample (2026-08-01)
-- MEASURE-ONLY. Time-forward: earlier games fit/observe, later games prove.
--
-- Deliberately RULER-INDEPENDENT, and that is the finding: reliability
-- (predicted vs actual hit rate) and resolution (does higher p_win hit more)
-- are both p_win-vs-outcome measures. No fair_prob appears anywhere below,
-- because none can. This is why the consensus ruler cannot change the
-- calibration verdict.
--
-- reliability = n-weighted mean |predicted - actual| across deciles. NOTE:
-- mean|p - outcome| on 0/1 rows is NOT calibration -- it is noise-dominated
-- individual error. Bucket first.
--
-- MLB only. WNBA abstains on its own data and is not re-litigated here.

--
-- 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 base as (
  select sport, game_date, p_win::numeric p, (outcome='hit')::int won,
         ntile(2) over (order by game_date, id) half
  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),
s as (select *, case when half=1 then 'train' else 'holdout' end split from base),
b as (select split, width_bucket(p, 0.0, 1.0, 10) bkt,
             count(*) n, avg(p) pred, avg(won::numeric) actual
      from s group by 1,2)
select split,
       sum(n) total_n,
       count(*) buckets,
       round(sum(n*abs(pred-actual))/sum(n),4)  reliability_mean_abs_dev,
       round((select corr(p, won::numeric) from s s2 where s2.split=b.split)::numeric,4) resolution_corr,
       round((select avg(won::numeric)     from s s3 where s3.split=b.split)::numeric,4) base_rate,
       (select min(game_date) from s s4 where s4.split=b.split) first_game,
       (select max(game_date) from s s5 where s5.split=b.split) last_game
from b group by split order by split desc;
