"""Score the Poly-US paper market maker.

WHY THIS EXISTS. mm_quoter_polyus.py has been quoting since 2026-07-29 and
writing mm_polyus_quotes.csv, but nothing ever read that file back. The Kalshi
book has mm_settle.py; this side had no scorer at all, so ~460k quote rows
across 264 books sat unmeasured -- on the one venue that CREDITS the maker
(MAKER_THETA = 0.0125 x p(1-p)).

SCORE BOOKS, NOT LEGS. A leg is not the unit of risk here. The quoter takes two
terminal actions that a leg-by-leg reading misses entirely, and an earlier
version of this file missed both:

  COMPLETE_AT_BID -- two legs have sold, so cross into the bid on the third
    near kickoff. The book is then short all three outcomes, exactly one of
    which pays: the P&L is LOCKED and outcome-independent. Holding these to
    settlement scores a position that was already closed.
  FLATTEN_1OF3 -- only one leg sold, so buy it back rather than carry a naked
    position into the match. Also closed before kickoff, also not a settlement.

Both write their realised per-contract economics into `reason` at the moment
they fire, computed with the live fee schedule. This reads those numbers rather
than recomputing them, for the same reason it reads `rebate_c` off the row: a
second implementation of the fee model is a second thing to drift.

WHAT A PASSIVE FILL MEANS, and why it is a CEILING. Poly-US exposes no trade
tape; `last_px` is a LEVEL sampled every 20s. A level already above our ask when
we start quoting is not evidence anyone traded with us, so a fill counts only
when last_px CHANGES to a value at or above our resting ask. Still optimistic --
it assumes we held queue priority for that print. Same caveat mm_settle carries.

Usage:  python main\\mm_settle_polyus.py
"""
import csv
import re
import shutil
import sys
import tempfile
from collections import defaultdict
from pathlib import Path

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

QUOTES = Path(r'E:\soccer\research\mm_polyus_quotes.csv')
RESULTS = Path(r'E:\soccer\research\mm_polyus_results.csv')
FIELDS = ['event_slug', 'div', 'match', 'kickoff', 'legs_sold', 'outcome',
          'pnl_c_per_contract', 'contracts', 'pnl_dollars', 'rebate_c', 'regime']

SIDE_WIN = {'home': 'H', 'draw': 'D', 'away': 'A'}
RE_LOCKED = re.compile(r'=>\s*([+-]?[\d.]+)c/contract locked')
RE_COST = re.compile(r'=>\s*cost\s*([+-]?[\d.]+)c/contract')


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


def load_quotes():
    """Rows grouped by leg, timestamp-ordered. Snapshotted first: the quoter
    holds the real file open and polls every 20s, and a partial read of a 96MB
    CSV mid-write is a silent wrong answer rather than an error."""
    tmp = Path(tempfile.gettempdir()) / 'mm_polyus_quotes.snap.csv'
    shutil.copyfile(QUOTES, tmp)
    by_leg = defaultdict(list)
    with tmp.open(newline='', encoding='utf-8', errors='replace') as fh:
        for r in csv.DictReader(fh):
            if r.get('market_slug'):
                by_leg[r['market_slug']].append(r)
    for v in by_leg.values():
        v.sort(key=lambda r: r.get('ts') or '')
    return by_leg


def results_map():
    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 {}
        out[(fx['gd'], fx['league'], r0.get('home'), r0.get('away'))] = fx['ftr']
    return out


def leg_state(rows):
    """(state, price, per_contract_c, rebate_c) for one leg.

    state is 'completed' (crossed into the bid), 'flattened' (bought back),
    'sold' (passive fill, still open at kickoff) or 'none'.
    """
    sold_px = None
    reb = 0.0
    prev = None
    for r in rows:
        reason = r.get('reason') or ''
        if 'COMPLETE_AT_BID' in reason:
            m = RE_LOCKED.search(reason)
            return 'completed', _f(r.get('our_ask')), (_f(m.group(1), 0.0) if m else 0.0), reb
        if 'FLATTEN_1OF3' in reason:
            m = RE_COST.search(reason)
            return 'flattened', _f(r.get('our_ask')), (_f(m.group(1), 0.0) if m else 0.0), reb
        ask, lp = _f(r.get('our_ask')), _f(r.get('last_px'))
        if sold_px is None and ask is not None and lp is not None and 0 < ask < 1:
            if prev is not None and lp != prev and lp >= ask:
                sold_px = ask
                reb = _f(r.get('rebate_c'), 0.0) or 0.0
        if lp is not None:
            prev = lp
    return ('sold', sold_px, None, reb) if sold_px is not None else ('none', None, None, 0.0)


def main():
    by_leg = load_quotes()
    res = results_map()

    books = defaultdict(list)
    for slug, rows in by_leg.items():
        books[rows[0].get('event_slug')].append((slug, rows))

    out, unresolved = [], 0
    for ev, legs in books.items():
        r0 = legs[0][1][0]
        gd = (r0.get('kickoff') or '')[:10]
        ftr = res.get((gd, (r0.get('div') or '').upper(),
                       r0.get('home'), r0.get('away')))
        states = {}
        for slug, rows in legs:
            side = (rows[0].get('side') or '').lower()
            states[side] = leg_state(rows)

        sold = [s for s in states.values() if s[0] in ('sold', 'completed')]
        n_sold = len(sold)
        flat = [s for s in states.values() if s[0] == 'flattened']
        completed = any(s[0] == 'completed' for s in states.values())
        if not sold and not flat:
            continue                              # never traded
        if ftr not in ('H', 'D', 'A') and not (completed or flat):
            unresolved += 1                       # unsettled AND still open
            continue

        size = _f(r0.get('size'), 10.0) or 10.0
        reb = sum(s[3] for s in states.values())

        # SIGN CONVENTIONS, and they are OPPOSITE -- this is worth stating because
        # reading them the same way silently flips the whole result:
        #   COMPLETE_AT_BID net_c = (collected - 1 + rebates)*100 - fee
        #       -> already P&L-signed, and ALREADY INCLUDES the rebates. Adding
        #          `reb` again here would double-count them.
        #   FLATTEN_1OF3  cost_c = (ask - sold)*100 + fee
        #       -> a COST. P&L is its NEGATION. It excludes the rebate earned on
        #          the original sale, so that is added back separately.
        flat_pnl = sum(-(s[2] or 0.0) for s in flat)
        if completed:
            per = next(s[2] for s in states.values() if s[0] == 'completed')
            outcome = 'complete'
        elif flat and n_sold == 0:
            per = flat_pnl + reb
            outcome = 'flattened'
        else:
            # Still open at kickoff: short n_sold outcomes, settle each.
            per = 0.0
            for side, s in states.items():
                if s[0] == 'sold' and s[1] is not None:
                    per += (s[1] - (1 if SIDE_WIN.get(side) == ftr else 0)) * 100.0
            per += flat_pnl + reb
            outcome = f'open {n_sold}/3'

        cts = size * max(n_sold + len(flat), 1)
        out.append({
            'event_slug': ev, 'div': r0.get('div'),
            'match': f"{r0.get('home')} vs {r0.get('away')}",
            'kickoff': r0.get('kickoff'), 'legs_sold': n_sold,
            'outcome': outcome, 'pnl_c_per_contract': round(per, 3),
            'contracts': cts, 'pnl_dollars': round(per * size / 100.0, 4),
            'rebate_c': round(reb, 4), 'regime': r0.get('regime')})

    with RESULTS.open('w', newline='', encoding='utf-8') as fh:
        w = csv.DictWriter(fh, fieldnames=FIELDS)
        w.writeheader()
        for o in out:
            w.writerow(o)
    print(summarise(out, len(books), unresolved))


def summarise(out, books_seen, unresolved):
    L = ['POLY-US PAPER MARKET MAKER  (book-level)', '',
         f'{books_seen} books in the quote log, {len(out)} that traded and can '
         f'be scored ({unresolved} traded but unsettled)', '']
    if not out:
        return '\n'.join(L + ['nothing scoreable yet'])

    tot = sum(o['pnl_dollars'] for o in out)
    L += [f'  books scored        : {len(out)}',
          f'  TOTAL P&L           : {tot:+,.2f} dollars', '']

    grp = defaultdict(lambda: [0, 0.0])
    for o in out:
        k = o['outcome'] if o['outcome'] in ('complete', 'flattened') else o['outcome']
        grp[k][0] += 1
        grp[k][1] += o['pnl_dollars']
    L += ['-' * 62, 'BY HOW THE BOOK ENDED', '',
          f'{"outcome":12} {"books":>6} {"P&L $":>10} {"per book":>10}']
    for k in sorted(grp, key=lambda x: -grp[x][1]):
        n, p = grp[k]
        L.append(f'{k:12} {n:>6} {p:>10,.2f} {p/n:>10,.3f}')
    L.append('')

    by_div = defaultdict(lambda: [0, 0.0])
    for o in out:
        by_div[o['div']][0] += 1
        by_div[o['div']][1] += o['pnl_dollars']
    L += ['-' * 62, 'BY LEAGUE', '']
    for d, (n, p) in sorted(by_div.items(), key=lambda kv: -kv[1][1]):
        L.append(f'  {d:6s} {n:>4} books  {p:+8,.2f} $  {p/n:+7.3f} $/book')
    L += ['', 'Complete books are OUTCOME-INDEPENDENT -- locked at the moment we',
          'crossed. Flattened books are closed positions. Only "open n/3" rows',
          'carry match risk. Passive fills assume queue priority, so those are',
          'a CEILING, same caveat mm_settle carries.']
    return '\n'.join(L)


if __name__ == '__main__':
    main()
