"""Is xlow's edge real, or is it just measuring a stale Pinnacle line?

No new data feed needed -- this answers the question with what we already have.

THE HYPOTHESIS. API-Football's "Pinnacle" repricess about once every 5.8h while
Kalshi moves every ~2.3h (measured 2026-07-28, 147 fixtures). A line that has not
moved for hours drifts away from a live exchange through pure time-lag, and that
drift is indistinguishable, in the CLV formula, from genuine mispricing. If that
is what xlow has been trading, then:

  (a) signals should fire disproportionately on OLD lines, and
  (b) bets entered on old lines should perform WORSE than bets on fresh ones.

(b) is the decisive one, because it uses realised results rather than theory.

LINE AGE = time since that fixture's (pin_home, pin_draw, pin_away) triple last
changed value, as of the snapshot the bet fired on. Age is measured only from the
first snapshot we have for the fixture, so early bets have censored ages -- those
are reported separately rather than silently counted as "fresh".

If old-line bets are the losers, the fix is free and immediate: gate xlow on line
age. That is worth knowing BEFORE paying for a real-time feed.
"""
import sys
from collections import defaultdict

sys.path.insert(0, r'E:\soccer\research')
import forward_test_unique_book as F

BAND = F.BAND


def age_series(fx):
    """[(snapshot_row, age_hours_or_None)] for a fixture's pre-KO rows."""
    out, last_val, last_t, first_t = [], None, None, None
    for r in fx['pre']:
        t = F._pts(r.get('timestamp'))
        if t is None:
            out.append((r, None))
            continue
        if first_t is None:
            first_t = t
        v = (r.get('pin_home'), r.get('pin_draw'), r.get('pin_away'))
        if v != last_val:
            last_val, last_t = v, t
        age = (t - last_t).total_seconds() / 3600 if last_t else None
        censored = (last_t == first_t)      # never seen it move yet
        out.append((r, (age, censored)))
    return out


def fire_with_age(fx):
    """Re-run fire_xlow, returning the winning bet plus the line age at entry."""
    for r, meta in age_series(fx):
        ph, pd_, pa = F._f(r.get('pin_home')), F._f(r.get('pin_draw')), F._f(r.get('pin_away'))
        if None in (ph, pd_, pa) or not (ph > 1 and pd_ > 1 and pa > 1):
            continue
        pf = F.fair_mult((ph, pd_, pa))
        cands = []
        for side, si in F.SIDX.items():
            km, ka = F.book_quote(r, 'kalshi', side)
            pm, pa_ = F.book_quote(r, 'poly', side)
            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, pm
            else:
                mid, px, other = pm, pa_, km
            if px is None or not (0 < px < 1):
                continue
            clv = (pf[si] - mid) / pf[si]
            oclv = (pf[si] - other) / pf[si]
            if not (mid < other and BAND[0] <= clv < BAND[1]):
                continue
            if BAND[0] <= oclv < BAND[1]:
                continue
            won = (fx['ftr'] == 'H' and side == 'home') or (fx['ftr'] == 'A' and side == 'away')
            cands.append(dict(clv=clv, px=px, won=won))
        if cands:
            b = max(cands, key=lambda d: d['clv'])
            b['age'] = meta[0] if meta else None
            b['censored'] = meta[1] if meta else True
            return b
    return None


def block(name, bets):
    s = F.stat(bets)
    if not s:
        print(f'  {name:22s} n=0')
        return
    print(f'  {name:22s} n={s["n"]:3d}  WR {s["wr"]:5.1f}%  impl {s["iwr"]:5.1f}%  '
          f'ROI {s["roi"]:+7.1f}%  z {s["z"]:+5.2f}')


def main():
    prepped = F.load_fixtures()
    oos = [fx for fx in prepped if fx['gd'] > F.FREEZE_DATE]
    bets = [b for b in (fire_with_age(fx) for fx in oos) if b]
    dated = [b for b in bets if b.get('age') is not None]
    print(f'{len(oos)} OOS fixtures -> {len(bets)} xlow bets, '
          f'{len(dated)} with a measurable line age\n')

    known = [b for b in dated if not b['censored']]
    cens = [b for b in dated if b['censored']]
    print(f'{len(known)} bets on a line we WATCHED move; {len(cens)} censored '
          f'(line had not moved since our first snapshot)\n')

    if known:
        ages = sorted(b['age'] for b in known)
        print('LINE AGE AT ENTRY (hours since Pinnacle last repriced)')
        print(f'  min {ages[0]:.1f}  median {ages[len(ages)//2]:.1f}  '
              f'max {ages[-1]:.1f}')
        print(f'  share entered on a line >3h old: '
              f'{sum(1 for a in ages if a > 3)/len(ages)*100:.0f}%\n')

        print('PERFORMANCE BY LINE AGE  (the decisive test)')
        cuts = [(0, 1), (1, 3), (3, 6), (6, 999)]
        for lo, hi in cuts:
            block(f'{lo}-{hi}h old', [b for b in known if lo <= b['age'] < hi])
        print()
        med = ages[len(ages) // 2]
        block(f'fresher than {med:.1f}h', [b for b in known if b['age'] < med])
        block(f'staler than {med:.1f}h', [b for b in known if b['age'] >= med])
    print()
    block('censored', cens)
    block('ALL (control)', bets)
    print('\nIf the stale buckets carry the losses, xlow has been partly trading')
    print('lag, and a line-age gate is a free fix that needs no new feed.')


if __name__ == '__main__':
    main()
