"""Why every completed book collected less than $1.00.

Today's live maker completed three books and every one of them came in under
the dollar it owed: SLAUNI 0.98, ALAGET 0.99, EXCPSV 1.00. The books were quoted
when their three asks summed to ~1.02. So the money did not vanish at quote
time, it vanished BETWEEN the first fill and the last.

Two candidate explanations, and they have opposite remedies:

  DRIFT     -- the overround decays with time-to-kickoff for everyone. Fixable
               by quoting later, closer to KO, where the decay is already spent.
  SELECTION -- a leg fills precisely when the market moves through it, and that
               same move makes the remaining legs worth less. Fixable ONLY by
               quoting wider than the expected move; waiting does nothing.

This measures both, then prices the premium needed to survive whichever is real.
"""
import csv, collections, statistics, sys, datetime as dt

QUOTES = r'E:\soccer\research\mm_paper_quotes.csv'
MAKER_FEE_RATE = 0.0175


def maker_fee(p):
    return MAKER_FEE_RATE * p * (1.0 - p)


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


def _ts(s):
    for f in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M'):
        try:
            return dt.datetime.strptime(str(s)[:19], f)
        except (ValueError, TypeError):
            pass
    return None


def load():
    """{event: {ts: {side: ask}}} plus kickoff per event."""
    books = collections.defaultdict(lambda: collections.defaultdict(dict))
    kos = {}
    with open(QUOTES, encoding='utf-8', errors='replace') as fh:
        for r in csv.DictReader(fh):
            ev, side = r.get('event'), (r.get('side') or '').lower()
            a = _f(r.get('k_ask'))
            t = _ts(r.get('ts'))
            if not (ev and side and a and t):
                continue
            books[ev][t][side] = a
            if ev not in kos:
                k = _ts(r.get('kickoff'))
                if k:
                    kos[ev] = k
    return books, kos


def complete_snaps(books, kos):
    """(event, ts, hours_to_ko, sum_of_three_asks) for snapshots with all 3 legs."""
    out = []
    for ev, snaps in books.items():
        ko = kos.get(ev)
        for t, legs in snaps.items():
            if len(legs) != 3:
                continue
            s = sum(legs.values())
            if not (0.8 < s < 1.4):
                continue
            h = (ko - t).total_seconds() / 3600.0 if ko else None
            out.append((ev, t, h, s))
    return out


def main():
    books, kos = load()
    snaps = complete_snaps(books, kos)
    print(f'{len(books):,} books, {len(snaps):,} snapshots with all three legs, '
          f'{len(kos):,} with a kickoff\n')

    # ── 1. DRIFT: overround against time to kickoff ──────────────────────
    print('=== 1. Is the overround decaying as kickoff approaches? ===')
    buckets = [(24, 999, '>24h'), (12, 24, '12-24h'), (6, 12, '6-12h'),
               (3, 6, '3-6h'), (1, 3, '1-3h'), (0.5, 1, '30-60m'),
               (0, 0.5, '<30m')]
    print(f'{"window":>8} {"n":>7} {"median sum":>11} {"p25":>7} {"p75":>7}')
    for lo, hi, lbl in buckets:
        v = sorted(s for _, _, h, s in snaps if h is not None and lo <= h < hi)
        if len(v) < 50:
            continue
        n = len(v)
        print(f'{lbl:>8} {n:>7,} {statistics.median(v):>11.4f} '
              f'{v[n//4]:>7.4f} {v[3*n//4]:>7.4f}')

    # ── 2. SELECTION: what happens to a book AFTER a leg would have filled ──
    # Proxy for "our ask got lifted": the leg's ask ROSE from one snapshot to
    # the next (someone bought up through where we were resting). Measure the
    # sum of the OTHER two legs before and after that event.
    print('\n=== 2. After one leg trades up, what happens to the other two? ===')
    deltas, base_sums = [], []
    for ev, snaps_by_t in books.items():
        times = sorted(t for t, legs in snaps_by_t.items() if len(legs) == 3)
        for a, b in zip(times, times[1:]):
            if (b - a).total_seconds() > 900:      # ignore gaps over 15 min
                continue
            la, lb = snaps_by_t[a], snaps_by_t[b]
            moved = [s for s in la if lb[s] - la[s] >= 0.01]
            if len(moved) != 1:
                continue
            hit = moved[0]
            rest = [s for s in la if s != hit]
            before = sum(la[s] for s in rest)
            after = sum(lb[s] for s in rest)
            deltas.append(after - before)
            base_sums.append(sum(la.values()))
    if deltas:
        d = sorted(deltas)
        n = len(d)
        print(f'  events: {n:,}')
        print(f'  median change in the other two legs: {statistics.median(d)*100:+.2f}c')
        print(f'  mean                               : {statistics.mean(d)*100:+.2f}c')
        print(f'  p25 / p75                          : '
              f'{d[n//4]*100:+.2f}c / {d[3*n//4]*100:+.2f}c')
        worse = sum(1 for x in d if x < 0)
        print(f'  share where the rest got CHEAPER   : {100.0*worse/n:.1f}%')
        print('\n  -> a maker who has sold one leg must now sell the other two')
        print('     into this. Negative means the book is worth less than when')
        print('     it was quoted, and that gap is the loss per completed book.')

    # ── 3. What premium makes it survivable? ─────────────────────────────
    print('\n=== 3. Premium required to complete profitably ===')
    if deltas:
        adverse = -statistics.median(deltas)     # cost per completed book, dollars
        med_sum = statistics.median([s for _, _, _, s in snaps])
        fee3 = 3 * maker_fee(med_sum / 3.0)
        print(f'  median quoted sum          : {med_sum:.4f}')
        print(f'  fees on three legs         : {fee3*100:.2f}c')
        print(f'  adverse move after 1st fill: {adverse*100:+.2f}c')
        need = 1.0 + fee3 + adverse
        print(f'  sum needed AT QUOTE TIME   : {need:.4f} '
              f'(= 1.00 + fees + the move)')
        ok = sum(1 for _, _, _, s in snaps if s >= need)
        print(f'  snapshots clearing that bar: {ok:,}/{len(snaps):,} '
              f'({100.0*ok/len(snaps):.1f}%)')
        print(f'  premium over the touch     : {(need-med_sum)*100:+.2f}c '
              f'({(need-med_sum)/3*100:+.2f}c per leg)')


if __name__ == '__main__':
    main()
