"""Does Kalshi lag Pinnacle, and is the gap big enough to take?

The maker is answered: 3.7% completion whatever we change, 2-4c a completion,
20-42c a stranded favourite, and Kalshi's rebate programme excludes Sports. So
this asks the other question the same data can answer -- not resting orders and
waiting, but crossing the spread when Kalshi is wrong.

The mechanism is specific. Pinnacle is the sharpest book in football; its line
IS fair value for practical purposes. Kalshi is a retail exchange that has to
find out. If Kalshi's price still reflects Pinnacle's OLD number some minutes
after Pinnacle moved, that gap is takeable.

What has to be true for it to pay:
  * the gap must exceed the TAKER fee, ~7% x p(1-p), so ~1.75c at even money
  * it must be biggest right after Pinnacle moves and decay after -- otherwise
    it is not lag, it is just Kalshi disagreeing, and disagreement is not edge
  * it must survive on liquid legs, not only on ones nobody trades

Measured on live_data: pin_home/draw/away are Pinnacle decimal odds, the
kalshi_*_mid columns are Kalshi. Pre-kickoff only, so no in-play confusion.
"""
import csv, collections, os, statistics, sys

sys.path.insert(0, r'E:\soccer\main')
sys.path.insert(0, r'E:\soccer\research')
import mm_settle as S

LIVE_DIR = r'E:\soccer\live_data'
SIDES = ('home', 'draw', 'away')
DAYS = 30
TAKER_RATE = 0.07


def taker_fee(p):
    return TAKER_RATE * p * (1.0 - p)


def _f(x, lo, hi):
    try:
        v = float(x)
        return v if lo < v < hi else None
    except (TypeError, ValueError):
        return None


def novig(odds):
    """Pinnacle decimal odds -> de-vigged probabilities (multiplicative)."""
    inv = [1.0 / o for o in odds]
    s = sum(inv)
    return [i / s for i in inv]


def load():
    """{fixture: [(ts, {side: (pin_fair, kalshi_mid)})]} pre-kickoff only."""
    files = sorted(f for f in os.listdir(LIVE_DIR) if f.endswith('.csv'))[-DAYS:]
    out = collections.defaultdict(list)
    for fn in files:
        with open(os.path.join(LIVE_DIR, fn), encoding='utf-8',
                  errors='replace') as fh:
            for r in csv.DictReader(fh):
                ts = S._pt(r.get('timestamp'))
                el = r.get('elapsed')
                if ts is None or el in (None, ''):
                    continue
                try:
                    if float(el) > 0:
                        continue                     # pre-kickoff only
                except ValueError:
                    continue
                po = [_f(r.get(f'pin_{s}'), 1.01, 200) for s in SIDES]
                km = [_f(r.get(f'kalshi_{s}_mid'), 0, 1) for s in SIDES]
                if any(x is None for x in po) or any(x is None for x in km):
                    continue
                fair = novig(po)
                key = (r.get('game_date'), r.get('home'), r.get('away'))
                out[key].append((ts, {s: (fair[i], km[i])
                                      for i, s in enumerate(SIDES)}))
    return out


def main():
    data = load()
    print(f'{len(data)} fixtures with both Pinnacle and Kalshi pre-kickoff')

    gaps = []                 # every (gap, mins_since_pin_moved, kalshi_mid)
    moves = 0
    for key, rows in data.items():
        rows.sort()
        last_move = {s: None for s in SIDES}
        prev = {s: None for s in SIDES}
        for ts, d in rows:
            for s in SIDES:
                pin, kal = d[s]
                if prev[s] is not None and abs(pin - prev[s]) >= 0.005:
                    last_move[s] = ts          # Pinnacle repriced this leg
                    moves += 1
                prev[s] = pin
                if last_move[s] is None:
                    continue
                mins = (ts - last_move[s]).total_seconds() / 60.0
                if mins > 120:
                    continue
                gaps.append((kal - pin, mins, kal))

    print(f'{moves:,} Pinnacle moves (>=0.5pp), {len(gaps):,} observations '
          f'within 2h of one\n')
    if len(gaps) < 500:
        print('too few')
        return

    absg = sorted(abs(g) for g, _, _ in gaps)
    n = len(absg)
    print('|Kalshi - Pinnacle fair|, all observations:')
    for lbl, p in (('median', .5), ('p75', .75), ('p90', .9), ('p99', .99)):
        print(f'   {lbl:>7} {absg[int(p*n)]*100:>6.2f}c')
    fee_at_even = taker_fee(0.5) * 100
    over = sum(1 for x in absg if x * 100 > fee_at_even)
    print(f'   above the ~{fee_at_even:.2f}c taker fee at even money: '
          f'{over:,}/{n:,} ({100.0*over/n:.1f}%)')

    print('\n=== the decisive test: does the gap DECAY after Pinnacle moves? ===')
    print('   if it does, it is lag and it is takeable.')
    print('   if it is flat, Kalshi simply disagrees, and that is not edge.\n')
    print(f'   {"minutes since move":>20} {"obs":>8} {"median |gap|":>13} '
          f'{"% over fee":>11}')
    for lo, hi in ((0, 2), (2, 5), (5, 10), (10, 20), (20, 45), (45, 120)):
        sel = [(abs(g), k) for g, m, k in gaps if lo <= m < hi]
        if len(sel) < 100:
            continue
        a = sorted(x for x, _ in sel)
        rich = sum(1 for x, k in sel if x > taker_fee(k))
        print(f'   {f"{lo}-{hi}m":>20} {len(sel):>8,} '
              f'{a[len(a)//2]*100:>12.2f}c {100.0*rich/len(sel):>10.1f}%')

    print('\n   (% over fee uses the fee at THAT leg\'s own price, not at 0.50)')


if __name__ == '__main__':
    main()
