"""
FORWARD TEST (read-only, trades nothing): executable late-drift momentum filter.

Frozen hypothesis (locked 2026-06-15, see project memory):
  A CLV signal is worth more if the Kalshi mid has ALREADY drifted toward our
  side by the time we bet. The executable, NON-look-ahead rule is:
     1. signal fires when clv_kalshi_<side> first enters [0.05, 0.12)  -> record entry mid
     2. at the LAST pre-KO snapshot (the decision point) check the mid drift
        drift = mid_late - mid_entry
     3. if drift >= 0 (momentum) -> "bet" at the LATE ask, p_win = late pin_fair
        if drift <  0 (dip)      -> skip
  Pricing at the late ask (not the early ask) is what makes it non-look-ahead;
  on these penny-quantized longshots the ask barely moves so it costs ~nothing.

In-sample (<= FREEZE_DATE) this beat baseline flat (+20.2% vs +11.6%) and held a
60/40 temporal split, but the Kelly cut was IS-worse/OOS-better (regime-artifact
signature) and the full-sample gap was only p=0.13. UNPROVEN -> forward test.

This script evaluates the FROZEN rule on fixtures with game_date > FREEZE_DATE
(genuine out-of-sample) and appends one dated row to forward_test_momentum_log.csv
each run, so the forward sample is tracked as it accumulates. NOTHING is traded.
"""
import csv, glob, os, math
from datetime import datetime, date
from collections import defaultdict

LIVE_DIR   = r"E:\soccer\live_data"
LOG_CSV    = r"E:\soccer\forward_test_momentum_log.csv"
FREEZE_DATE = "2026-06-14"          # last in-sample game_date; > this = forward/OOS
BAND        = (0.05, 0.12)
EXCLUDE_LEAGUES = set()   # 2026-06-17: no longer excluding USL/BSA
KELLY_MULT, MAX_BET_PCT, MIN_STAKE, BANK0 = 0.25, 0.25, 1.0, 1000.0

_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 build_signals(entry_posedge=False):
    """One bet per fixture (both-sides dedupe -> higher CLV), with entry + late pricing.

    entry_posedge=False (default, FROZEN forward-test rule): enter at the FIRST snapshot
        whose clv_kalshi is in BAND, regardless of whether the ask still clears pin_fair.
    entry_posedge=True (live-mirror): enter at the first in-band snapshot whose ASK is
        already below pin_fair (positive edge at the ask). This matches what live_monitor
        actually does -- it fires the first cycle it sees a qualifying, positive-edge ask --
        and is still NON-look-ahead (acts on the first favorable tick, not the global min).
    """
    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)

    sigs = []
    for (gd, league, home, away), rows in fixtures.items():
        if not gd or league in EXCLUDE_LEAGUES:
            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
        cands = []
        for side in ('home', 'away'):
            clvk, midk, askk, pfk = (f'clv_kalshi_{side}', f'kalshi_{side}_mid',
                                     f'kalshi_{side}_ask', f'pin_fair_{side}')
            ent = None
            for ts, r in pre:
                c = _f(r.get(clvk))
                if c is None or not (BAND[0] <= c < BAND[1]):
                    continue
                if entry_posedge:
                    av, pv = _f(r.get(askk)), _f(r.get(pfk))
                    if av is None or pv is None or not (0 < av < 1) or not (pv > av):
                        continue   # wait for the first tick where the ask clears pin_fair
                ent = (ts, r, c); break
            if not ent:
                continue
            m0 = _f(ent[1].get(midk)); a0 = _f(ent[1].get(askk)); p0 = _f(ent[1].get(pfk))
            if m0 is None or a0 is None or p0 is None or not (0 < a0 < 1):
                continue
            mL = aL = pL = None
            for ts, r in reversed(pre):
                mv, av, pv = _f(r.get(midk)), _f(r.get(askk)), _f(r.get(pfk))
                if mv is not None and av is not None and pv is not None:
                    mL, aL, pL = mv, av, pv; break
            if mL is None or not (0 < aL < 1):
                continue
            won = (ftr[0] == 'H' and side == 'home') or (ftr[0] == 'A' and side == 'away')
            cands.append(dict(gd=gd, league=league, side=side, clv=ent[2],
                              a0=a0, p0=p0, aL=aL, pL=pL, won=won, drift=mL - m0))
        if cands:
            sigs.append(max(cands, key=lambda d: d['clv']))   # higher-CLV side
    sigs.sort(key=lambda s: s['gd'])
    return sigs


def _kelly(p, a):
    if not (0 < a < 1): return 0.0
    f = (p - a) / (1 - a)
    return min(max(f, 0.0) * KELLY_MULT, MAX_BET_PCT)

FLAT_DOLLAR = 10.0   # canonical flat stake on the BANK0=$1000 paper bankroll

def evaluate(bets, signal, staking):
    """One of the tracked strategies.
       signal  : 'baseline' -> all bets, EARLY ask (a0/p0)
                 'momentum' -> drift>=0,  LATE ask (aL/pL)
                 'edge'     -> pin_fair>ask, LATE ask (aL/pL); positive-edge SELECTION
                               with NO drift requirement -- isolates the edge gate.
       staking : 'flat'  -> fixed $10 stake every bet
                 'kelly' -> 0.25x fractional Kelly, $1 floor, 25% cap; SKIPS zero-edge bets.
       Returns n, win-rate, total return %, per-bet geometric growth %, max-drawdown %,
       and ROI-on-turnover % (net / dollars staked)."""
    bank = BANK0; n = 0; w = 0; peak = BANK0; mdd = 0.0; staked = 0.0; net = 0.0
    late = signal in ('momentum', 'edge')
    for b in bets:
        if signal == 'momentum' and b['drift'] < 0:
            continue
        a = b['aL'] if late else b['a0']
        p = b['pL'] if late else b['p0']
        if not (0 < a < 1):
            continue
        if signal == 'edge' and not (p > a):   # positive-edge selection (late ask < fair)
            continue
        unitnet = (1 - a) / a if b['won'] else -1.0
        if staking == 'kelly':
            frac = _kelly(p, a)
            if frac <= 0:
                continue                       # no positive edge -> Kelly stakes nothing
            stake = min(max(frac * bank, MIN_STAKE), bank)
        else:
            stake = FLAT_DOLLAR
        bank += stake * unitnet
        staked += stake; net += stake * unitnet
        n += 1; w += int(b['won'])
        peak = max(peak, bank); mdd = min(mdd, (bank - peak) / peak)
    return dict(n=n,
                wr=(w / n * 100) if n else 0.0,
                total=(bank / BANK0 - 1) * 100,
                g=(((bank / BANK0) ** (1.0 / n) - 1) * 100) if n else 0.0,
                mdd=mdd * 100,
                roiturn=(net / staked * 100) if staked else 0.0,
                final=bank)

# the strategies we forward-track: (tag, signal, staking, label)
# 'ef' (edge flat) added 2026-06-17: positive-edge SELECTION, late-ask priced, flat-staked.
# Isolates the edge gate (pin_fair>ask) from drift -- the one empty cell of the
# {baseline,momentum,edge} x {flat,kelly} grid (edge+kelly == baseline kelly, a duplicate).
COMBOS = [('bf', 'baseline', 'flat',  'baseline flat'),
          ('bk', 'baseline', 'kelly', 'baseline kelly'),
          ('mf', 'momentum', 'flat',  'momentum flat'),
          ('mk', 'momentum', 'kelly', 'momentum kelly'),
          ('ef', 'edge',     'flat',  'edge flat')]


def log_forward_snapshot(sigs=None):
    """Compute forward (OOS) metrics for all four combos and append one dated row
    to LOG_CSV. Returns the row dict. Called by main() and by the weekly pipeline
    (weekly_bet_summary.py) so the log grows on schedule."""
    if sigs is None:
        sigs = build_signals()
    forward = [s for s in sigs if s['gd'] > FREEZE_DATE]
    row = {'run_date': date.today().isoformat(), 'freeze_date': FREEZE_DATE}
    for tag, sig, stk, nm in COMBOS:
        d = evaluate(forward, sig, stk) if forward else None
        row[f'{tag}_n']       = d['n'] if d else 0
        row[f'{tag}_total']   = round(d['total'], 2)   if d else ''
        row[f'{tag}_g']       = round(d['g'], 4)       if d else ''
        row[f'{tag}_mdd']     = round(d['mdd'], 2)     if d else ''
        row[f'{tag}_roiturn'] = round(d['roiturn'], 2) if d else ''
    new = not os.path.exists(LOG_CSV)
    with open(LOG_CSV, 'a', newline='', encoding='utf-8') as fh:
        wri = csv.DictWriter(fh, fieldnames=list(row.keys()))
        if new: wri.writeheader()
        wri.writerow(row)
    return row


def main():
    sigs = build_signals()
    insample = [s for s in sigs if s['gd'] <= FREEZE_DATE]
    forward  = [s for s in sigs if s['gd'] >  FREEZE_DATE]

    print(f"FREEZE_DATE={FREEZE_DATE}   in-sample n={len(insample)}   FORWARD n={len(forward)}")
    for label, bets in [("IN-SAMPLE (reference)", insample), ("FORWARD (OOS)", forward)]:
        print(f"\n--- {label} ---")
        if not bets:
            print("  (no fixtures yet)")
            continue
        for tag, sig, stk, nm in COMBOS:
            d = evaluate(bets, sig, stk)
            print(f"  {nm:16s} n={d['n']:3d} WR={d['wr']:4.1f}% total={d['total']:+7.1f}% "
                  f"g/bet={d['g']:+.4f}% MDD={d['mdd']:+6.1f}% ROIturn={d['roiturn']:+6.1f}%")

    log_forward_snapshot(sigs)
    print(f"\nlogged forward snapshot ({len(COMBOS)} combos) -> {LOG_CSV}")


if __name__ == "__main__":
    main()
