"""Do Kalshi prices drift TOWARD Pinnacle fair?

The maker strategy caps our ask at the market's ask, so we only ever collect the
market's own price. That is fine if the market is already right, and costly if the
market is systematically about to move toward pin_fair -- in that case we are
selling a leg that is about to get cheaper (good) or, worse, selling the leg on the
wrong side of a move we could have anticipated.

METHOD
  For each leg, walk its quote history on a NON-OVERLAPPING grid of horizon H.
    gap(t)  = pin_fair(t) - k_mid(t)          (cents; + means Kalshi too cheap)
    dmid(t) = k_mid(t+H)  - k_mid(t)          (cents; how the market actually moved)
  If Kalshi drifts toward Pinnacle, dmid should share gap's sign and be some
  fraction of it.

WHY NON-OVERLAPPING: consecutive 20-second snapshots of the same leg are almost
the same observation. Overlapping windows would inflate n by ~200x and make any
t-stat meaningless. We also cluster standard errors by EVENT, because the three
legs of one match move together mechanically (they sum to ~1).

CAVEAT this cannot escape: ~1.4 days of data, 48 events. This is a directional
read, not a verdict. The historical xlow work (n=395, +0.30c) is the real estimate.
"""
import csv
import datetime as dt
import statistics
import sys
from collections import defaultdict

PATH = 'research/mm_paper_quotes.csv'
MIN_GAP_C = 0.5      # ignore noise-level disagreements
HORIZONS_MIN = [15, 30, 60, 120]


def _pt(s):
    if not s:
        return None
    try:
        d = dt.datetime.fromisoformat(s.replace('Z', '+00:00'))
    except ValueError:
        return None
    return d if d.tzinfo else d.replace(tzinfo=dt.timezone.utc)


def load():
    legs = defaultdict(list)
    with open(PATH, encoding='utf-8') as fh:
        for r in csv.DictReader(fh):
            if not (r['pin_fair'] and r['k_bid'] and r['k_ask']):
                continue
            t = _pt(r['ts'])
            ko = _pt(r['kickoff'])
            if t is None or ko is None or t >= ko:
                continue                      # pre-kickoff only
            bid, ask = float(r['k_bid']), float(r['k_ask'])
            legs[(r['event'], r['side'])].append({
                't': t,
                'fair': float(r['pin_fair']) * 100,
                'mid': (bid + ask) / 2 * 100,
                'event': r['event'],
            })
    for v in legs.values():
        v.sort(key=lambda x: x['t'])
    return legs


def samples(legs, horizon_min):
    """Non-overlapping (gap, dmid) pairs."""
    H = dt.timedelta(minutes=horizon_min)
    out = []
    for rows in legs.values():
        i = 0
        while i < len(rows):
            t0 = rows[i]['t']
            j = None
            for k in range(i + 1, len(rows)):
                if rows[k]['t'] - t0 >= H:
                    j = k
                    break
            if j is None:
                break
            gap = rows[i]['fair'] - rows[i]['mid']
            dmid = rows[j]['mid'] - rows[i]['mid']
            out.append({'gap': gap, 'dmid': dmid, 'event': rows[i]['event']})
            i = j                              # <- no overlap
    return out


def clustered_t(vals_by_cluster):
    """t-stat on the mean, treating each cluster as one observation."""
    means = [statistics.mean(v) for v in vals_by_cluster.values() if v]
    if len(means) < 3:
        return float('nan'), float('nan'), 0
    m = statistics.mean(means)
    sd = statistics.stdev(means)
    se = sd / (len(means) ** 0.5)
    return m, (m / se if se else float('nan')), len(means)


def main():
    legs = load()
    print(f'{len(legs)} legs, {len(set(k[0] for k in legs))} events\n')

    for H in HORIZONS_MIN:
        s = samples(legs, H)
        wide = [x for x in s if abs(x['gap']) >= MIN_GAP_C]
        if not wide:
            print(f'H={H:>4}min  no samples with |gap| >= {MIN_GAP_C}c')
            continue

        # Signed drift: positive = moved toward pin_fair.
        by_ev = defaultdict(list)
        for x in wide:
            by_ev[x['event']].append(x['dmid'] * (1 if x['gap'] > 0 else -1))
        mean_toward, t, nclust = clustered_t(by_ev)

        moved = [x for x in wide if abs(x['dmid']) > 1e-9]
        hit = sum(1 for x in moved
                  if (x['dmid'] > 0) == (x['gap'] > 0)) / len(moved) * 100 if moved else float('nan')
        frac = mean_toward / statistics.mean([abs(x['gap']) for x in wide]) * 100

        print(f'H={H:>4}min  n={len(wide):5d} obs / {nclust:2d} events   '
              f'drift toward fair {mean_toward:+.3f}c  (t={t:+.2f}, clustered)  '
              f'| moved-right {hit:4.1f}%  | closes {frac:+.0f}% of gap')

    # Where does the drift live -- by size of disagreement, at the horizon that
    # matters most for us (1h, roughly how long a resting order sits).
    print('\nBY GAP SIZE (H=60min):')
    s = samples(legs, 60)
    buckets = [(0.5, 1.0), (1.0, 2.0), (2.0, 3.0), (3.0, 99.0)]
    print(f"  {'|gap|':>12s} {'n':>5s} {'drift toward fair':>19s} {'t':>7s} {'moved-right':>12s}")
    for lo, hi in buckets:
        b = [x for x in s if lo <= abs(x['gap']) < hi]
        if len(b) < 10:
            print(f'  {f"{lo}-{hi}c":>12s} {len(b):5d}   (too few)')
            continue
        by_ev = defaultdict(list)
        for x in b:
            by_ev[x['event']].append(x['dmid'] * (1 if x['gap'] > 0 else -1))
        m, t, nc = clustered_t(by_ev)
        mv = [x for x in b if abs(x['dmid']) > 1e-9]
        hit = sum(1 for x in mv if (x['dmid'] > 0) == (x['gap'] > 0)) / len(mv) * 100 if mv else float('nan')
        print(f'  {f"{lo}-{hi}c":>12s} {len(b):5d} {m:+18.3f}c {t:+7.2f} {hit:11.1f}%')

    # Reference: what does a round trip cost?
    print('\nFor scale: crossing a 1c spread costs 0.5c; the taker fee at 25c is '
          f'{0.07*0.25*0.75*100:.2f}c.')


if __name__ == '__main__':
    sys.exit(main())
