"""
Does the RAW SIZE of the Kalshi-vs-Poly CLV gap predict profit, with NO band
gate at all (i.e. bet the cheaper book regardless of whether either leg
clears [5,12) vs Pinnacle)?  NO.

Distinct from two prior negative results this complements:
  - kalshi_poly_pure_deviation.py: fair = mean(kalshi,poly), no Pinnacle at all -> dead.
  - forward_test_unique_book.py fire_xlow(): the LIVE rule, which REQUIRES the
    cheap leg's clv-vs-Pinnacle to land in [5,12) AND the other leg to sit
    OUTSIDE that band.
This script asks the question in between: keep Pinnacle as the reference for
each leg's CLV (so it's not "pure" deviation), but drop the band gate
entirely and ask whether the raw GAP SIZE between clv_kalshi and clv_poly
(both vs pin fair) is itself predictive, continuously, regardless of where
either value falls.

METHOD: first pre-KO snapshot per fixture; for each side, cheap leg = lower-
priced book, gap = clv(cheap leg) - clv(other leg) [always >= 0]; pick the
side with the single largest gap; settle at the cheap book's ask (Kalshi
ask / Poly est-ask). Bet UNCONDITIONALLY -- no band requirement anywhere.

RESULT on poly_backfill (643 fixtures with a resolvable K/P gap, 2026-07-16):
  Disjoint gap buckets are NOT monotonic (the tell something's wrong):
    [0,2)%   n=305  ROI -4.0%   z-1.05
    [2,5)%   n=255  ROI +4.2%   z-0.01
    [5,8)%   n= 50  ROI -24.1%  z-0.86   <- dips hard, breaks monotonicity
    [8,12)%  n= 22  ROI -8.6%   z+0.86
    [12,20)% n= 11  ROI +77.5%  z+1.37   <- only n=11, ~7% implied (longshot)

  Cumulative thresholds all collapse to a handful of ultra-longshot bets as T
  rises (n: 643->83->33->8 as T: 0%->5%->8%->15%); the ONE "significant" cell
  (gap>=15%, n=8, ROI+144%, z+2.06, p=.02) is 8 bets at ~7% implied odds --
  one win at ~14:1 explains the whole result. Classic small-cell longshot mirage,
  same shape as the round-robin/sub-band traps already catalogued.

  OOS 60/40 split KILLS every threshold tried:
    gap>=0%: TRAIN -8.2% -> TEST +9.6% (sign flip, both near zero -- just noise)
    gap>=5%: TRAIN +5.6% -> TEST -23.9% (sign flip)
    gap>=8%: TRAIN -12.9% -> TEST +64.9% (sign flip, both small n)
  No threshold holds sign across the split.

VERDICT: raw cross-book CLV-gap magnitude carries NO exploitable edge without
the band gate. This is not a contradiction of the frozen strict-xlow rule --
it's confirmation that the band conditions (cheap leg IN [5,12), other leg
OUT of [5,12)) are doing real selection work, not just being an arbitrary
cutoff. Don't drop or loosen the band based on gap size alone.

Read-only, backtest only. Mirrors kalshi_poly_pure_deviation.py conventions.
"""
import csv, glob, math
from datetime import datetime
from collections import defaultdict

DIR = r"E:\soccer\poly_backfill"
_FMTS = ['%Y-%m-%d %H:%M:%S', '%m/%d/%Y %H:%M', '%m/%d/%Y %H:%M:%S', '%Y-%m-%d %H:%M']

def _pts(s):
    for f in _FMTS:
        try: return datetime.strptime(s, f)
        except (ValueError, TypeError): pass
    return None
def _f(x):
    try: return float(x)
    except (TypeError, ValueError): return None
def _result(rows):
    for r in reversed(rows):
        v = (r.get('ftr') or '').upper().strip()
        if v in ('H', 'D', 'A'): return v
    return None
def fair_mult(o):
    r = [1.0/x for x in o]; s = sum(r); return [ri/s for ri in r]
def half_spread(mid):
    if   mid < 0.05: return 0.003
    elif mid < 0.25: return 0.005
    elif mid < 0.35: return 0.025
    return 0.020

def load():
    fixtures = defaultdict(list)
    for path in glob.glob(f"{DIR}\\*_polybackfill.csv"):
        with open(path, newline='', encoding='utf-8') as fh:
            for r in csv.DictReader(fh):
                fixtures[(r.get('game_date'), r.get('home'), r.get('away'))].append(r)
    out = []
    for key, rows in fixtures.items():
        gd = key[0]
        if not gd: continue
        pre = sorted([(_pts(r['timestamp']), r) for r in rows
                      if (r.get('status') or '').upper() == 'PRE' and _pts(r['timestamp'])],
                     key=lambda t: t[0])
        ftr = _result(rows)
        if pre and ftr:
            out.append(dict(gd=gd, ftr=ftr, pre=[r for _, r in pre]))
    out.sort(key=lambda f: f['gd'])
    return out

SIDX = {'home': 0, 'away': 2}

def pick_no_band(fx):
    """First pre-KO snapshot: pick the side/book with the LARGEST clv-vs-Pinnacle
    gap between Kalshi and Poly. No band requirement anywhere. Bet unconditionally."""
    for r in fx['pre']:
        ph, pd_, pa = _f(r.get('pin_home')), _f(r.get('pin_draw')), _f(r.get('pin_away'))
        if None in (ph, pd_, pa) or not (ph > 1 and pd_ > 1 and pa > 1): continue
        pf = fair_mult((ph, pd_, pa)); cands = []
        for side, si in SIDX.items():
            km = _f(r.get(f'kalshi_{side}_mid')); ka = _f(r.get(f'kalshi_{side}_ask'))
            pm = _f(r.get(f'poly_{side}_price'))
            if km is None or pm is None or not (0 < km < 1 and 0 < pm < 1): continue
            if km <= pm: mid, px, other = km, (ka if ka and 0 < ka < 1 else km), pm
            else:        mid, px, other = pm, pm + half_spread(pm), km
            clv = (pf[si] - mid) / pf[si]; oclv = (pf[si] - other) / pf[si]
            gap = clv - oclv
            won = (fx['ftr'] == 'H' and side == 'home') or (fx['ftr'] == 'A' and side == 'away')
            cands.append(dict(clv=clv, gap=gap, side=side, px=px, won=won, gd=fx['gd']))
        if cands:
            return max(cands, key=lambda d: d['gap'])
    return None

def stat(bets):
    n = len(bets)
    if not n: return None
    w = sum(b['won'] for b in bets)
    net = sum(((1-b['px'])/b['px']) if b['won'] else -1.0 for b in bets)
    mu = sum(b['px'] for b in bets); var = sum(b['px']*(1-b['px']) for b in bets)
    z = (w-mu)/math.sqrt(var) if var > 0 else 0.0
    return dict(n=n, w=w, wr=w/n*100, iwr=mu/n*100, roi=net/n*100, z=z,
                p=0.5*math.erfc(z/math.sqrt(2)))

def line(tag, s):
    if not s: print(f"  {tag:26s} (none)"); return
    print(f"  {tag:26s} n={s['n']:3d}  {s['w']}W-{s['n']-s['w']}L  WR={s['wr']:4.1f}% "
          f"(impl {s['iwr']:4.1f}%)  ROI={s['roi']:+7.1f}%  z={s['z']:+.2f}  p={s['p']:.4f}")

def main():
    F = load()
    bets = [b for b in (pick_no_band(f) for f in F) if b]
    bets.sort(key=lambda b: b['gd'])
    print(f"Total fixtures with a K/P gap on at least one side: {len(bets)}\n")

    print("=== Disjoint buckets by GAP SIZE ===")
    for lo, hi in [(0, 0.02), (0.02, 0.05), (0.05, 0.08), (0.08, 0.12), (0.12, 0.20), (0.20, 1.0)]:
        sub = [b for b in bets if lo <= b['gap'] < hi]
        line(f"gap [{lo*100:.0f},{hi*100:.0f})%", stat(sub))

    print("\n=== Cumulative gap thresholds (bet if gap >= T, no cap) ===")
    for T in (0.0, 0.02, 0.03, 0.05, 0.08, 0.10, 0.12, 0.15, 0.20):
        sub = [b for b in bets if b['gap'] >= T]
        line(f"gap >= {T*100:.0f}%", stat(sub))

    print("\n=== OOS 60/40 check ===")
    for T in (0.0, 0.05, 0.08):
        sub = sorted([b for b in bets if b['gap'] >= T], key=lambda b: b['gd'])
        cut = int(len(sub) * 0.6)
        line(f"T={T*100:.0f}% TRAIN", stat(sub[:cut]))
        line(f"T={T*100:.0f}% TEST", stat(sub[cut:]))

if __name__ == '__main__':
    main()
