"""Is market making on Poly-US worth it? Settle the LIVE maker's books.

WHAT THIS ANSWERS. project_mm_completion_arithmetic reduces a maker to one
number: the completion rate, against a break-even set by what a complete book
pays versus what a partial one costs. That was measured on KALSHI. Poly-US has
different economics -- it CREDITS the maker 0.0125*p(1-p) where Kalshi CHARGES
0.0175*p(1-p) -- so the break-even is its own number and has never been
measured on live fills.

The paper-era scorer (main/mm_settle_polyus.py, regime `pus-v1`) inferred fills
from the tape. This does not: it reads the LIVE maker's own state, where a leg
is marked filled only from a real position, so every book here is real money.

METHOD. For each event the maker sold at least one leg of:
    P&L = sum(prices of legs SOLD) - (1.00 if the winning outcome was one we
          sold, else 0.00) + maker rebate
A COMPLETE book always sells the winner, so it is deterministically
sum(px) - 1.00. A PARTIAL book is a coin flip on whether the outcome it is
short actually lands -- which is the whole risk of the strategy.

Read-only. Run:  python research/pus_maker_pnl.py
"""
import csv
import json
import os
import sys
from collections import defaultdict

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.join(HERE, '..', 'main'))

STATE = os.path.join(HERE, 'pus_live_state.json')
LEGS = os.path.join(HERE, 'pus_live_legs.csv')

MAKER_REBATE = 0.0125          # Poly-US CREDITS the maker this * p(1-p)


def _f(x, d=0.0):
    try:
        return float(x)
    except (TypeError, ValueError):
        return d


def settled_books():
    """event -> {leg: (net, realized)} from Poly-US's OWN settlement record.

    REPLACES the state-derived version (2026-08-28). pus_live_state.json is
    written by detect_fills, which reads positions and prints
    'positions unreadable, not marking fills' on every 503 -- and this venue
    503s for hours at a time. Fills that landed while it was blind were simply
    never recorded, so the state undercounts BOOKS badly: 16 settled books in
    state against 62 the venue has resolved.

    The same class of bug was found on Kalshi the same day, where a lossy local
    join reported 45 complete books against the exchange's 115. Completion is
    the number this strategy is judged on; it cannot come from a local record
    that stops writing when a venue hiccups.

    ACTIVITY_TYPE_POSITION_RESOLUTION carries beforePosition.netPosition (what
    we held into settlement) and afterPosition.realized (what the venue paid or
    took). Using the venue's own realized figure removes the need to reconstruct
    P&L from prices and outcomes at all -- no fixture lookup, no side mapping,
    no result join.

    Returns None if activities cannot be read.
    """
    try:
        sys.path.insert(0, os.path.join(HERE, '..', 'main'))
        import poly_us_auth as pa
        import requests
    except Exception as e:
        print(f'  [warn] poly-US auth unavailable: {type(e).__name__}: {e}')
        return None
    out, cursor, pages = defaultdict(dict), None, 0
    try:
        while pages < 40:
            params = {'limit': 100}
            if cursor:
                params['cursor'] = cursor
            r = requests.get(pa.API_BASE + '/v1/portfolio/activities',
                             headers=pa._auth_headers('GET', '/v1/portfolio/activities'),
                             params=params, timeout=25)
            if r.status_code != 200:
                raise RuntimeError(f'{r.status_code}: {r.text[:120]}')
            d = r.json()
            for a in (d.get('activities') or []):
                if a.get('type') != 'ACTIVITY_TYPE_POSITION_RESOLUTION':
                    continue
                pr = a.get('positionResolution') or {}
                slug = pr.get('marketSlug')
                bp, ap = pr.get('beforePosition') or {}, pr.get('afterPosition') or {}
                net = _f((bp.get('netPosition')))
                if not slug or not net:
                    continue
                real = _f(((ap.get('realized') or {}).get('value')))
                ev, leg = slug.rsplit('-', 1)
                out[ev][leg] = (net, real)
            cursor, pages = d.get('nextCursor'), pages + 1
            if not cursor or d.get('eof'):
                break
    except Exception as e:
        print(f'  [warn] activities unreadable ({type(e).__name__}: {e})')
        return None
    return dict(out)


def side_map():
    """slug -> 'home' | 'draw' | 'away', from the leg log."""
    out = {}
    with open(LEGS, newline='', encoding='utf-8') as fh:
        for r in csv.DictReader(fh):
            s = (r.get('side') or '').strip().lower()
            if r.get('slug') and s:
                out[r['slug']] = s
    return out


def opening_gross():
    """event slug -> gross_c the FIRST time the book was quoted.

    The gate (GROSS_GATE_C, 2.0c) decides which books open, and the 2026-08-24
    read found 16 of 18 risked books opened at exactly the gate minimum and
    completed 50% -- against a 74.2% break-even. A book opened on 2c of
    overround has almost nothing to spread across the remaining legs once one
    fills, so the ratchet holds out near the touch and strands.

    FIRST row, not min or mean: the question is what the book was worth when
    the decision to open it was made. Later rows carry the ratchet's own
    repricing, which is a consequence of that decision, not an input to it.
    """
    out = {}
    with open(LEGS, newline='', encoding='utf-8') as fh:
        for r in csv.DictReader(fh):
            slug = r.get('slug') or ''
            if not slug:
                continue
            ev = slug.rsplit('-', 1)[0]
            if ev in out:
                continue
            g = _f(r.get('gross_c'), None)
            if g is not None:
                out[ev] = g
    return out


def results_map():
    """(home, away) -> 'H'/'D'/'A' for every settled fixture."""
    import forward_test_unique_book as F
    out = {}
    for fx in F.load_fixtures(require_result=True):
        r0 = fx['pre'][0] if fx['pre'] else {}
        h, a = r0.get('home'), r0.get('away')
        if h and a:
            out[(h.strip(), a.strip())] = fx['ftr']
    return out


def maker_events():
    """Event prefixes the MAKER actually quoted, from its own leg log.

    WHY THIS FILTER IS NOT OPTIONAL. /v1/portfolio/activities is the ACCOUNT's
    history, not the maker's: it carries manual bets, xlow-era trades and World
    Cup futures going back to June, while the live maker only started
    2026-08-22. Without this, `tec-f-wc-2026-07-19-groupd-winner` lands in a
    market-making completion rate as a 1-leg 'partial book' worth -$1.00.

    The leg log is the record of what this maker offered, so an event absent
    from it was never its book.
    """
    out = set()
    try:
        with open(LEGS, newline='', encoding='utf-8') as fh:
            for r in csv.DictReader(fh):
                slug = r.get('slug') or ''
                if '-' in slug:
                    out.add(slug.rsplit('-', 1)[0])
    except OSError as e:
        print(f'  [warn] leg log unreadable ({e}); cannot filter to maker books')
        return None
    return out


def main():
    books_ex = settled_books()
    if books_ex is not None:
        quoted = maker_events()
        if quoted:
            before = len(books_ex)
            books_ex = {e: v for e, v in books_ex.items() if e in quoted}
            print(f'venue settled books: {before}  ->  {len(books_ex)} the maker '
                  f'actually quoted (rest are manual/xlow/futures)')
        return report_exchange(books_ex)
    print('  falling back to the local state file -- expect undercounting')
    return main_from_state()


def report_exchange(books):
    """Completion and P&L straight off the venue's settlement record."""
    rows = []
    for ev, legs in books.items():
        realized = sum(r for _, r in legs.values() if r is not None)
        rows.append({'ev': ev, 'n': len(legs), 'pnl_c': realized * 100.0,
                     'complete': len(legs) >= 3})
    if not rows:
        print('no settled books yet')
        return 0
    comp = [r for r in rows if r['complete']]
    part = [r for r in rows if not r['complete']]
    tot = sum(r['pnl_c'] for r in rows)

    print(f'settled books (venue record): {len(rows)}   '
          f'(complete {len(comp)}, partial {len(part)})')
    print()
    print(f'{"book":<46}{"legs":>5}{"P&L c":>10}')
    for r in sorted(rows, key=lambda x: x['pnl_c'])[:12]:
        print(f'  {r["ev"][-44:]:<44}{r["n"]:>5}{r["pnl_c"]:>10.2f}')
    print('  ...')
    for r in sorted(rows, key=lambda x: x['pnl_c'])[-5:]:
        print(f'  {r["ev"][-44:]:<44}{r["n"]:>5}{r["pnl_c"]:>10.2f}')
    print()

    def _s(label, xs):
        if not xs:
            print(f'{label:<26} (none)')
            return
        m = sum(x['pnl_c'] for x in xs) / len(xs)
        print(f'{label:<26} n={len(xs):>3}   mean {m:>+8.2f}c/book   '
              f'total {sum(x["pnl_c"] for x in xs):>+9.1f}c')

    _s('COMPLETE books', comp)
    _s('PARTIAL books', part)
    _s('ALL books', rows)
    print()
    print(f'completion rate: {100*len(comp)/len(rows):.1f}%')
    if comp and part:
        cg = sum(x['pnl_c'] for x in comp) / len(comp)
        pc = sum(x['pnl_c'] for x in part) / len(part)
        if cg != pc:
            print(f'break-even completion rate: {-pc/(cg-pc)*100:.1f}%   '
                  f'(complete {cg:+.2f}c vs partial {pc:+.2f}c)')
    print()
    print(f'REALISED (venue-reported): ${tot/100.0:+.2f} over {len(rows)} books')
    print('P&L is afterPosition.realized summed per book -- the venue own '
          'number, not a reconstruction.')
    return 0


def main_from_state():
    st = json.load(open(STATE, encoding='utf-8'))
    sides = side_map()
    res = results_map()

    books = defaultdict(dict)       # event_slug -> {slug: (qty, px)}
    names = {}
    for slug, v in st.items():
        if not isinstance(v, dict):
            continue
        ev = slug.rsplit('-', 1)[0]
        if v.get('event'):
            names[ev] = v['event']
        q = _f(v.get('fill_qty'))
        if q > 0:
            books[ev][slug] = (q, _f(v.get('fill_px')))

    rows, unmatched = [], []
    for ev, legs in books.items():
        name = names.get(ev, '')
        if ' vs ' not in name:
            unmatched.append((ev, name, len(legs)))
            continue
        home, away = [x.strip() for x in name.split(' vs ', 1)]
        ftr = res.get((home, away))
        if ftr is None:
            unmatched.append((ev, name, len(legs)))
            continue
        want = {'H': 'home', 'D': 'draw', 'A': 'away'}[ftr]
        collected = sum(px for _, px in legs.values())
        qty = max(q for q, _ in legs.values())
        sold_sides = {sides.get(s) for s in legs}
        paid = 1.00 if want in sold_sides else 0.0
        rebate = sum(MAKER_REBATE * px * (1 - px) for _, px in legs.values())
        pnl_c = (collected - paid + rebate) * 100.0
        rows.append({'ev': ev, 'name': name, 'n': len(legs), 'qty': qty,
                     'collected': collected, 'paid': paid, 'pnl_c': pnl_c,
                     'complete': len(legs) >= 3})

    if not rows:
        print('no settled live books yet')
        return 0

    comp = [r for r in rows if r['complete']]
    part = [r for r in rows if not r['complete']]
    tot = sum(r['pnl_c'] for r in rows)

    print(f'settled live books: {len(rows)}   '
          f'(complete {len(comp)}, partial {len(part)})')
    print(f'unsettled / unmatched: {len(unmatched)}')
    print()
    print(f'{"book":<44}{"legs":>5}{"collected":>11}{"paid":>7}{"P&L c":>9}')
    for r in sorted(rows, key=lambda x: x['pnl_c']):
        print(f'  {r["name"][:42]:<42}{r["n"]:>5}{r["collected"]:>11.2f}'
              f'{r["paid"]:>7.2f}{r["pnl_c"]:>9.2f}')
    print()

    def _s(label, xs):
        if not xs:
            print(f'{label:<26} (none)')
            return
        m = sum(x['pnl_c'] for x in xs) / len(xs)
        print(f'{label:<26} n={len(xs):>3}   mean {m:>+7.2f}c/book   '
              f'total {sum(x["pnl_c"] for x in xs):>+8.1f}c')

    _s('COMPLETE books', comp)
    _s('PARTIAL books', part)
    _s('ALL books', rows)
    print()
    rate = len(comp) / len(rows) * 100 if rows else 0
    print(f'completion rate: {rate:.1f}%')
    if comp and part:
        cg = sum(x['pnl_c'] for x in comp) / len(comp)
        pc = sum(x['pnl_c'] for x in part) / len(part)
        if cg != pc:
            be = -pc / (cg - pc) * 100
            print(f'break-even completion rate: {be:.1f}%   '
                  f'(complete {cg:+.2f}c vs partial {pc:+.2f}c)')
    print()
    print(f'REALISED, at {rows[0]["qty"]:.0f} contract(s)/leg: '
          f'${tot / 100.0 * rows[0]["qty"]:+.2f} over {len(rows)} books')

    # ── COMPLETION AND P&L BY OPENING GROSS ────────────────────────────────
    # The gate is the only lever that plausibly moves completion, so this is
    # the table the whole question turns on. Read it as a HYPOTHESIS CHECK:
    # on 2026-08-24 the wide buckets held 1 book each, which is not evidence.
    # What would count is the 2-3c bucket staying under break-even while a
    # populated wider bucket clears it.
    og = opening_gross()
    buckets = [('2-3c', 2.0, 3.0), ('3-5c', 3.0, 5.0),
               ('5-8c', 5.0, 8.0), ('8c+', 8.0, 1e9)]
    agg = {b[0]: {'n': 0, 'comp': 0, 'pnl': 0.0} for b in buckets}
    missing = 0
    for r in rows:
        g = og.get(r['ev'])
        if g is None:
            missing += 1
            continue
        for name, lo, hi in buckets:
            if lo <= g < hi:
                a = agg[name]
                a['n'] += 1
                a['comp'] += 1 if r['complete'] else 0
                a['pnl'] += r['pnl_c']
                break
    print()
    print('COMPLETION AND P&L BY OPENING GROSS (settled books):')
    print(f'  {"gross":<7}{"n":>4}{"complete":>10}{"rate":>8}{"mean c/book":>13}')
    for name, _, _ in buckets:
        a = agg[name]
        if not a['n']:
            print(f'  {name:<7}{0:>4}{"—":>10}{"—":>8}{"—":>13}')
            continue
        print(f'  {name:<7}{a["n"]:>4}{a["comp"]:>10}'
              f'{100 * a["comp"] / a["n"]:>7.1f}%{a["pnl"] / a["n"]:>+13.2f}')
    if missing:
        print(f'  ({missing} book(s) had no opening gross recorded)')
    print('  NOTE: a single-book bucket is not evidence. The gate only moves if')
    print('        a POPULATED wider bucket clears break-even while 2-3c does not.')
    return 0


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