"""
Band sweep under HONEST (power) de-vig: which CLV band is actually +EV, and is it
stable out-of-sample?  Read-only, no betting.

For each candidate band [lo,hi), re-run the frozen selection (first pre-KO snapshot
whose power-de-vig mid-CLV is in band; both-sides dedupe to higher CLV; bet at that
snapshot's ask; settle by ftr). Report n, WR, implied(ask) WR, flat ROIturn, and the
one-sided win-rate-vs-implied z (Poisson-binomial).

Sweeping bands IS multiple testing, so for each band we ALSO show a 60/40 temporal
split (first 60% of fixtures by date = "train", last 40% = "test"). A band whose edge
lives only in the train half is an in-sample mirage, not a real band.
"""
import csv, glob, os, math
from datetime import datetime
from collections import defaultdict

LIVE_DIR = r"E:\soccer\live_data"
_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_power(o):
    r = [1.0/x for x in o]
    lo, hi = 0.5, 10.0
    for _ in range(60):
        k = (lo+hi)/2
        s = sum(ri**k for ri in r)
        if s > 1.0: lo = k
        else:       hi = k
    k = (lo+hi)/2
    p = [ri**k for ri in r]; s = sum(p)
    return [pi/s for pi in p]

# Pre-load fixtures once; cache per-snapshot power-CLV so the sweep is fast.
def load_fixtures():
    fixtures = defaultdict(list)
    for path in glob.glob(os.path.join(LIVE_DIR, "*.csv")):
        with open(path, newline='', encoding='utf-8') as fh:
            for r in csv.DictReader(fh):
                fixtures[(r.get('game_date'), r.get('league'),
                          r.get('home'), r.get('away'))].append(r)
    # precompute, per fixture, the time-ordered list of (clv_home, ask_home, clv_away, ask_away)
    prepped = []
    for (gd, league, home, away), rows in fixtures.items():
        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])
        if len(pre) < 2: continue
        ftr = _result(rows)
        if ftr is None: continue
        snaps = []
        for ts, r in 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_power((ph, pd_, pa))
            row = {}
            for side, sidx in (('home', 0), ('away', 2)):
                mid = _f(r.get(f'kalshi_{side}_mid')); ask = _f(r.get(f'kalshi_{side}_ask'))
                if mid is None or ask is None or not (0 < ask < 1):
                    row[side] = None; continue
                clv = (pf[sidx] - mid) / pf[sidx] if pf[sidx] else None
                row[side] = (clv, ask)
            snaps.append(row)
        if snaps:
            prepped.append(dict(gd=gd, league=league, ftr=ftr, snaps=snaps,
                                won_home=(ftr == 'H'), won_away=(ftr == 'A')))
    prepped.sort(key=lambda x: x['gd'])
    return prepped

def select(prepped, lo, hi):
    bets = []
    for fx in prepped:
        cands = []
        for side in ('home', 'away'):
            for snap in fx['snaps']:
                cell = snap.get(side)
                if cell is None: continue
                clv, ask = cell
                if clv is None or not (lo <= clv < hi): continue
                won = fx['won_home'] if side == 'home' else fx['won_away']
                cands.append((clv, ask, won)); break
        if cands:
            clv, ask, won = max(cands, key=lambda t: t[0])
            bets.append(dict(gd=fx['gd'], ask=ask, won=won))
    bets.sort(key=lambda b: b['gd'])
    return bets

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

def main():
    prepped = load_fixtures()
    print(f"Loaded {len(prepped)} resolved fixtures with pre-KO snapshots.\n")
    bands = [(0.02,0.05),(0.03,0.07),(0.05,0.12),(0.05,0.20),(0.07,0.15),
             (0.07,0.20),(0.10,0.20),(0.10,0.30),(0.12,0.30),(0.05,1.0),
             (0.10,1.0),(0.02,1.0)]
    print(f"{'band':14s} {'n':>4} {'WR%':>5} {'impl%':>6} {'ROIturn%':>9} {'z':>6} {'p':>6}   "
          f"| 60/40 split  trainROI(z)   testROI(z)")
    for lo, hi in bands:
        bets = select(prepped, lo, hi)
        s = stat(bets)
        if not s:
            print(f"[{lo:.2f},{hi:.2f})  (no bets)"); continue
        cut = int(len(bets)*0.6)
        tr, te = stat(bets[:cut]), stat(bets[cut:])
        line = (f"[{lo:.2f},{hi:.2f})  {s['n']:4d} {s['wr']:5.1f} {s['iwr']:6.1f} "
                f"{s['roi']:+9.1f} {s['z']:+6.2f} {s['p']:6.3f}   | ")
        if tr and te:
            line += (f"tr:{tr['roi']:+6.1f}%(z{tr['z']:+.2f}) "
                     f"te:{te['roi']:+6.1f}%(z{te['z']:+.2f})")
        print(line)
    print("\nNote: lowest-p band here is IN-SAMPLE optimized over 12 bands. Trust the")
    print("60/40 split (does the edge survive in the test half?) over the headline p.")

if __name__ == '__main__':
    main()
