"""
Poly edge NET OF SPREAD: re-price the mid-only backfill bets at an estimated ASK.

The live spread sample (research/poly_spread_sample.py, 2026-06-27) gave median
half-spreads on open longshot markets (mid 0.05-0.35) of ~0.7-3 cents. Apply that
bucketed half-spread to the backfill mids to estimate ask = mid + half, then settle
the [0.05,0.12) bets at the ask instead of the mid. Shows how much of Poly's +18-19%
mid ROI survives crossing the spread. Selection still fires on the mid-CLV signal (as
the live pipeline defines it); only the execution price changes.

Also reports an OPTIMISTIC (half the sampled spread) and PESSIMISTIC (2x) band so the
answer is a range, since the live sample is small/noisy and date-contaminated.
"""
import csv, glob, os, 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 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
        if sum(ri**k for ri in r) > 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]
_DEVIG = {'mult': fair_mult, 'power': fair_power}

# bucketed median half-spread from the NEAR-TERM live sample (poly_spread_nearterm.py,
# 118 World Cup/BSA/CSL fixtures, 2026-06-27) -- the right comparable for pre-KO bets.
def half_spread(mid, mult=1.0):
    if   mid < 0.05: h = 0.003
    elif mid < 0.10: h = 0.005
    elif mid < 0.15: h = 0.005
    elif mid < 0.25: h = 0.005
    elif mid < 0.35: h = 0.025
    else:            h = 0.020
    return h * mult

def load_fixtures():
    fixtures = defaultdict(list)
    for path in glob.glob(os.path.join(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('league'),
                          r.get('home'), r.get('away'))].append(r)
    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 not pre: continue
        ftr = _result(rows)
        if ftr is None: continue
        prepped.append(dict(gd=gd, ftr=ftr, pre=[r for _, r in pre]))
    prepped.sort(key=lambda x: x['gd'])
    return prepped

def select(prepped, devig, lo, hi):
    fn = _DEVIG[devig]; bets = []
    for fx in prepped:
        cands = []
        for side, sidx in (('home', 0), ('away', 2)):
            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
                mid = _f(r.get(f'poly_{side}_price'))
                if mid is None or not (0 < mid < 1): continue
                pf = fn((ph, pd_, pa))[sidx]
                clv = (pf - mid) / pf if pf else None
                if clv is None or not (lo <= clv < hi): continue
                won = (fx['ftr'] == 'H' and side == 'home') or (fx['ftr'] == 'A' and side == 'away')
                cands.append((clv, mid, won)); break
        if cands:
            clv, mid, won = max(cands, key=lambda t: t[0])
            bets.append(dict(mid=mid, won=won))
    return bets

def score(bets, price_fn):
    n = len(bets)
    if not n: return None
    wins = sum(b['won'] for b in bets)
    net = mu = var = 0.0; skipped = 0
    for b in bets:
        px = price_fn(b['mid'])
        if not (0 < px < 1): skipped += 1; continue
        net += ((1-px)/px) if b['won'] else -1.0
        mu += px; var += px*(1-px)
    m = n - skipped
    roi = net/m*100 if m else 0
    z = (wins - mu)/math.sqrt(var) if var > 0 else 0.0
    p = 0.5*math.erfc(z/math.sqrt(2))
    return dict(n=m, wr=wins/m*100 if m else 0, roi=roi, z=z, p=p)

def main():
    prepped = load_fixtures()
    for dv in ('mult', 'power'):
        bets = select(prepped, dv, 0.05, 0.12)
        print(f"\n=== {dv} de-vig, band [0.05,0.12), n={len(bets)} ===")
        rows = [("at MID (no spread)",   lambda m: m),
                ("ask = mid + 0.5x half", lambda m: m + half_spread(m, 0.5)),
                ("ask = mid + 1x half",   lambda m: m + half_spread(m, 1.0)),
                ("ask = mid + 2x half",   lambda m: m + half_spread(m, 2.0))]
        print(f"  {'pricing':22s} {'n':>4} {'WR%':>5} {'ROIturn%':>9} {'z':>6} {'p':>6}")
        for label, fn in rows:
            s = score(bets, fn)
            if s:
                print(f"  {label:22s} {s['n']:4d} {s['wr']:5.1f} {s['roi']:+9.1f} "
                      f"{s['z']:+6.2f} {s['p']:6.3f}")

if __name__ == '__main__':
    main()
