"""
Is the Kalshi market wising up?  Measure the Kalshi-vs-Pinnacle price GAP over
calendar time, on ALL fixtures (not just settled bets) -- a far higher-n signal
than win/loss P&L.

For each fixture take the LAST pre-KO Kalshi snapshot (its "closing" line, the
fairest comparison to Pinnacle's close). For the bettable home/away sides compute
  pin_fair  = multiplicative de-vig of (pin_home,pin_draw,pin_away)   [live default]
  kalshi_mid
  gap       = kalshi_mid - pin_fair          (signed; <0 means Kalshi cheap = +CLV)
  |gap|     = mispricing magnitude
  CLV       = (pin_fair - kalshi_mid)/pin_fair

CONVERGENCE (market wising up) => |gap| shrinks and mean CLV -> 0 over time.

CONFOUND: league mix shifts over the season (Apr = European majors, Jun = thin
summer leagues w/ structurally bigger gaps), so a raw time trend can be pure mix.
We therefore ALSO report the trend WITHIN each league that spans several weeks,
which removes the mix. Edge-relevant view = longshot sides pin_fair in [0.05,0.40].
Read-only.
"""
import csv, glob, os, math
from datetime import datetime, date
from collections import defaultdict

DIR = r"E:\soccer\live_data"
LO, HI = 0.05, 0.40   # longshot pin_fair range where the strategy bets
_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 fair_mult(o):
    r = [1.0/x for x in o]; s = sum(r); return [ri/s for ri in r]
def isoweek(gd):
    try: d = datetime.strptime(gd, '%Y-%m-%d').date()
    except ValueError: return None
    y, w, _ = d.isocalendar(); return f"{y}-W{w:02d}"

def load_sides():
    """Return list of dicts: one per (fixture, side) at the closing pre-KO snapshot."""
    fixtures = defaultdict(list)
    for path in glob.glob(os.path.join(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)
    out = []
    for (gd, league, home, away), rows in fixtures.items():
        if not gd: continue
        pre = [(_pts(r['timestamp']), r) for r in rows
               if (r.get('status') or '').upper() == 'PRE' and _pts(r['timestamp'])]
        if not pre: continue
        _, r = max(pre, key=lambda t: t[0])           # closing pre-KO snapshot
        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
        fair = fair_mult((ph, pd_, pa))
        wk = isoweek(gd)
        for side, sidx in (('home', 0), ('away', 2)):
            mid = _f(r.get(f'kalshi_{side}_mid'))
            if mid is None or not (0 < mid < 1): continue
            pf = fair[sidx]
            out.append(dict(wk=wk, gd=gd, league=league, side=side, pf=pf, mid=mid,
                            gap=mid - pf, agap=abs(mid - pf),
                            clv=(pf - mid)/pf if pf else None,
                            longshot=(LO <= pf < HI)))
    return out

def summ(rows):
    n = len(rows)
    if not n: return None
    agap = sum(r['agap'] for r in rows)/n
    gap = sum(r['gap'] for r in rows)/n
    clvs = [r['clv'] for r in rows if r['clv'] is not None]
    clv = sum(clvs)/len(clvs) if clvs else float('nan')
    fpos = sum(1 for r in rows if r['clv'] is not None and r['clv'] >= 0.05)/n
    return dict(n=n, agap=agap*100, gap=gap*100, clv=clv*100, fpos=fpos*100)

def corr(xs, ys):
    n = len(xs)
    if n < 3: return float('nan')
    mx, my = sum(xs)/n, sum(ys)/n
    cov = sum((x-mx)*(y-my) for x, y in zip(xs, ys))
    sx = math.sqrt(sum((x-mx)**2 for x in xs)); sy = math.sqrt(sum((y-my)**2 for y in ys))
    return cov/(sx*sy) if sx > 0 and sy > 0 else float('nan')

def main():
    sides = load_sides()
    ls = [r for r in sides if r['longshot']]
    print(f"closing-snapshot sides total={len(sides)}, longshot[{LO},{HI})={len(ls)}\n")

    # 1) raw weekly trend on longshot sides (CONFOUNDED by league mix)
    byw = defaultdict(list)
    for r in ls: byw[r['wk']].append(r)
    print("Weekly longshot Kalshi-vs-Pinnacle gap (RAW -- confounded by league mix):")
    print(f"  {'week':9s} {'nSides':>6} {'|gap|pp':>8} {'gap_pp':>7} {'meanCLV%':>9} "
          f"{'%CLV>=5':>8} {'nLg':>4}  topLeagues")
    weeks = sorted(byw)
    idx = []
    for i, wk in enumerate(weeks):
        rows = byw[wk]; s = summ(rows)
        lgc = defaultdict(int)
        for r in rows: lgc[r['league']] += 1
        top = ",".join(f"{l}:{c}" for l, c in sorted(lgc.items(), key=lambda kv: -kv[1])[:4])
        print(f"  {wk:9s} {s['n']:6d} {s['agap']:8.2f} {s['gap']:+7.2f} {s['clv']:+9.2f} "
              f"{s['fpos']:7.1f}% {len(lgc):4d}  {top}")
        idx.append((i, s))
    print(f"\n  corr(week_index, |gap|)   = {corr([i for i,_ in idx], [s['agap'] for _,s in idx]):+.2f}")
    print(f"  corr(week_index, meanCLV) = {corr([i for i,_ in idx], [s['clv'] for _,s in idx]):+.2f}")
    print("  (positive |gap| corr => WIDENING; negative => converging. Mix-confounded.)")

    # 2) WITHIN-LEAGUE trend (removes mix): slope of weekly |gap| vs week, for
    #    leagues present in >=3 distinct weeks with >=15 longshot sides total.
    print("\nWithin-league |gap| trend (mix-controlled): leagues spanning >=3 weeks:")
    print(f"  {'lg':6s} {'nSides':>6} {'weeks':>5} {'|gap|first':>10} {'|gap|last':>10} "
          f"{'slope_pp/wk':>12} {'corr':>6}")
    bylg = defaultdict(list)
    for r in ls: bylg[r['league']].append(r)
    wk_to_i = {wk: i for i, wk in enumerate(weeks)}
    trends = []
    for lg, rows in sorted(bylg.items()):
        wkrows = defaultdict(list)
        for r in rows: wkrows[r['wk']].append(r)
        if len(wkrows) < 3 or len(rows) < 15: continue
        pts = sorted((wk_to_i[wk], summ(rs)['agap']) for wk, rs in wkrows.items())
        xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
        # OLS slope
        n = len(xs); mx = sum(xs)/n; my = sum(ys)/n
        den = sum((x-mx)**2 for x in xs)
        slope = sum((x-mx)*(y-my) for x, y in zip(xs, ys))/den if den else float('nan')
        c = corr(xs, ys)
        trends.append(slope)
        print(f"  {lg:6s} {len(rows):6d} {len(wkrows):5d} {ys[0]:10.2f} {ys[-1]:10.2f} "
              f"{slope:+12.2f} {c:+6.2f}")
    if trends:
        pos = sum(1 for s in trends if s > 0)
        print(f"\n  within-league slopes: {len(trends)} leagues, {pos} widening / "
              f"{len(trends)-pos} converging, median slope = "
              f"{sorted(trends)[len(trends)//2]:+.2f} pp/week")
        print("  (consistent NEGATIVE within-league slopes = market wising up;"
              " mixed/near-zero = no convergence signal)")

if __name__ == '__main__':
    main()
