-- p_win RECALIBRATION — per-sport, time-forward holdout. Measure-only.
-- specs/pwin-recalibration-holdout.md. NOTE: reliability is measured on BUCKETS
-- (predicted vs actual rate, n-weighted). mean|p-outcome| on 0/1 rows is NOT
-- calibration — it is noise-dominated individual error.
with r as (
  select sport, game_date, p_win::numeric p, (outcome='hit')::int won,
         ntile(2) over (partition by sport order by game_date) half
  from ledger_entries
  where user_id is null and outcome in ('hit','miss') and p_win is not null
), tagged as (
  select *, case when half=1 then 'train' else 'holdout' end split,
         width_bucket(p,0.1,1.0,5) bkt, ln(p/(1-p)) lg from r
), tb as (   -- TRAIN buckets = the fit
  select sport, bkt, count(*) n, avg(lg) mean_lg, avg(won::numeric) rate
  from tagged where split='train' group by sport, bkt having count(*) >= 8
), platt as (
  select sport,
    regr_slope(ln(greatest(least(rate,.98),.02)/(1-greatest(least(rate,.98),.02))), mean_lg) b,
    regr_intercept(ln(greatest(least(rate,.98),.02)/(1-greatest(least(rate,.98),.02))), mean_lg) a
  from tb group by sport
), hb as (
  select h.sport, h.bkt, count(*) n, avg(h.p) pred_raw,
    avg(1/(1+exp(-(pl.a+pl.b*h.lg)))) pred_platt,
    avg(coalesce(tb.rate,h.p)) pred_iso, avg(h.won::numeric) actual
  from tagged h join platt pl on pl.sport=h.sport
  left join tb on tb.sport=h.sport and tb.bkt=h.bkt
  where h.split='holdout' group by h.sport,h.bkt having count(*) >= 8
)
select sport, sum(n) holdout_n, count(*) buckets,
  sum(n*abs(pred_raw-actual))/sum(n)   raw_reliability_dev,
  sum(n*abs(pred_platt-actual))/sum(n) platt_reliability_dev,
  sum(n*abs(pred_iso-actual))/sum(n)   isotonic_reliability_dev
from hb group by sport order by sport;
-- Resolution (ordering survives?) is the corr(pred, won) variant of the same CTEs.
