"""Who are the PROFITABLE market makers in Polymarket soccer, and what do they do?

(read-only; touches no account and places nothing)

WHY. Our own maker is stranded 13.8% of the time against an 11.1% break-even,
and the open question is whether assembling a 3-leg book is even the right
construct. Polymarket settles on-chain, so unlike Kalshi -- where trades are
anonymous and only AGGREGATE maker P&L is recoverable (research/mm_feasibility.py)
-- individual makers can be named, ranked by realised P&L, and studied.

Polymarket models a soccer match as THREE binary markets ("Will X win?", "...end
in a draw?", "Will Y win?"), which is exactly our three legs, so the comparison
is structural rather than analogical.

HOW A MAKER IS IDENTIFIED. data-api /trades returns the TAKER side only by
default; with takerOnly=false it returns BOTH sides of every fill. The set
difference on (transactionHash, proxyWallet) is therefore the maker side.
Verified 2026-09-14: 901 of 942 hashes resolve to clean 2-row pairs and 53% of
rows classify as maker, which is the ~half you would expect.

P&L. Per wallet per market: cash flow (BUY -size*px, SELL +size*px) plus the
settlement value of the net position (outcomePrices is ["1","0"]-style and
aligns with outcomeIndex). A negative net position is the complement leg and the
same arithmetic holds.

READ THE OUTPUT SCEPTICALLY:
  * SURVIVORSHIP. These are wallets that are currently large. Anyone who ran the
    same strategy into the ground is absent, which is exactly the inference
    error that produced the South America mirage.
  * NO QUEUE DATA. Fills are visible; resting orders are not. Nothing here can
    speak to queue position or refresh cadence.
  * DIFFERENT ECONOMICS. Polymarket's fee and tick structure is not Kalshi's,
    and our verdict rests on the 1c tick being the binding constraint. Findings
    transfer as HYPOTHESES.
  * SIZE. The top wallet moved 2.2M on one match. What works while warehousing
    that inventory need not work at $1/leg.
  * The offset cap on /trades is ~10k rows per market, so the very largest
    markets are TRUNCATED (reported per market as `truncated`).
"""
import collections
import json
import os
import sys
import time

import requests

GAMMA = 'https://gamma-api.polymarket.com'
DATA = 'https://data-api.polymarket.com'
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pm_maker_study.csv')
PAGE = 500
OFFSET_CAP = 10000          # the API refuses beyond roughly this


def _get(url, params, tries=4):
    for i in range(tries):
        try:
            r = requests.get(url, params=params, timeout=30)
        except Exception:
            time.sleep(1.5 * (i + 1)); continue
        if r.status_code == 200:
            return r.json()
        if r.status_code == 400:        # offset past the cap -- not an error
            return None
        time.sleep(1.5 * (i + 1))
    return None


def match_events(n_events, closed=True):
    """Highest-volume soccer matches that have the 3-leg structure."""
    out = []
    for off in (0, 100, 200, 300, 400):
        evs = _get(f'{GAMMA}/events',
                   {'limit': 100, 'offset': off, 'tag_slug': 'soccer',
                    'closed': str(closed).lower(), 'order': 'volume',
                    'ascending': 'false'})
        if not evs:
            break
        for e in evs:
            ms = e.get('markets') or []
            if len(ms) == 3 and ' vs' in (e.get('title') or '').lower():
                if all(m.get('outcomePrices') for m in ms):
                    vol = sum(float(m.get('volume') or 0) for m in ms)
                    out.append((vol, e))
        if len(out) >= n_events * 3:
            break
    out.sort(key=lambda kv: -kv[0])
    return [e for _, e in out[:n_events]]


def trades(cid, taker_only):
    rows, off = [], 0
    while off < OFFSET_CAP:
        p = {'market': cid, 'limit': PAGE, 'offset': off}
        if not taker_only:
            p['takerOnly'] = 'false'
        d = _get(f'{DATA}/trades', p)
        if not d:
            break
        rows += d
        off += len(d)
        if len(d) < PAGE:
            break
        time.sleep(0.12)
    return rows, off >= OFFSET_CAP


def study(n_events=6, complete_only=True, pool=400):
    """Aggregate maker behaviour over matches with COMPLETE trade history.

    THE TRUNCATION PROBLEM. /trades refuses offset > ~10500, and before/after
    filters are ignored, so the biggest markets return only their most RECENT
    10k rows per leg -- a slice pressed right up against settlement, where
    trades happen at 0.999/0.001 and are near-riskless cleanup rather than
    market making. Measuring "legs quoted per match" on that window biases
    hard toward 1, because a maker who worked all three legs during the match
    but only one at the end reads as a one-leg maker.

    So: walk DOWN the volume ranking and keep only matches whose every leg
    came back under the cap. Smaller markets, but the history is whole and the
    number means what it says.
    """
    evs = match_events(pool) if complete_only else match_events(n_events)
    print(f'{len(evs)} candidate matches with the 3-leg structure'
          f'{" (selecting complete-history ones)" if complete_only else ""}\n')
    # wallet -> stats
    W = collections.defaultdict(lambda: {
        'pnl': 0.0, 'maker_fills': 0, 'taker_fills': 0, 'size': 0.0,
        'events': set(), 'legs': collections.defaultdict(set)})
    used = 0
    for e in evs:
        if used >= n_events:
            break
        title = (e.get('title') or '').encode('ascii', 'replace').decode()
        # Pre-flight: is this match's history complete? Probe each leg at the
        # cap; if anything is still returning rows there, the market is bigger
        # than we can page and the sample would be settlement-skewed.
        if complete_only:
            trunc_any = False
            for m in e['markets']:
                d = _get(f'{DATA}/trades', {'market': m.get('conditionId'),
                                            'limit': 1, 'offset': OFFSET_CAP - 200,
                                            'takerOnly': 'false'})
                if d:
                    trunc_any = True
                    break
            if trunc_any:
                continue
        used += 1
        print(f'  {title[:52]:<52}', end='', flush=True)
        n_rows = 0
        for m in e['markets']:
            cid = m.get('conditionId')
            try:
                prices = [float(x) for x in json.loads(m.get('outcomePrices'))]
            except Exception:
                continue
            allr, trunc = trades(cid, False)
            tk, _ = trades(cid, True)
            tkey = {(t['transactionHash'], t['proxyWallet']) for t in tk}
            n_rows += len(allr)
            # cash + position per wallet for THIS leg
            cash = collections.defaultdict(float)
            posn = collections.defaultdict(lambda: collections.defaultdict(float))
            for t in allr:
                w = t['proxyWallet']
                try:
                    sz = float(t.get('size') or 0); px = float(t.get('price') or 0)
                    oi = int(t.get('outcomeIndex') or 0)
                except (TypeError, ValueError):
                    continue
                is_maker = (t['transactionHash'], w) not in tkey
                if is_maker:
                    W[w]['maker_fills'] += 1
                else:
                    W[w]['taker_fills'] += 1
                W[w]['size'] += sz
                W[w]['events'].add(e.get('slug'))
                W[w]['legs'][e.get('slug')].add(cid)
                if (t.get('side') or '').upper() == 'BUY':
                    cash[w] -= sz * px; posn[w][oi] += sz
                else:
                    cash[w] += sz * px; posn[w][oi] -= sz
            for w, c in cash.items():
                settle = sum(q * (prices[i] if i < len(prices) else 0.0)
                             for i, q in posn[w].items())
                W[w]['pnl'] += c + settle
        print(f' {n_rows:>6} rows')
    return W


def main():
    n = int(sys.argv[1]) if len(sys.argv) > 1 else 6
    W = study(n)
    rows = []
    for w, s in W.items():
        tot = s['maker_fills'] + s['taker_fills']
        if tot < 20:
            continue
        mshare = s['maker_fills'] / tot
        legs = [len(v) for v in s['legs'].values()]
        rows.append({
            'wallet': w, 'pnl': round(s['pnl'], 2), 'fills': tot,
            'maker_share': round(mshare, 3), 'size': round(s['size']),
            'events': len(s['events']),
            'avg_legs_per_event': round(sum(legs) / len(legs), 2) if legs else 0,
            'all3_event_share': round(sum(1 for L in legs if L == 3) / len(legs), 2) if legs else 0,
        })
    rows.sort(key=lambda r: -r['pnl'])
    import csv
    with open(OUT, 'w', newline='', encoding='utf-8') as fh:
        w_ = csv.DictWriter(fh, fieldnames=list(rows[0].keys()) if rows else
                            ['wallet', 'pnl', 'fills', 'maker_share', 'size',
                             'events', 'avg_legs_per_event', 'all3_event_share'])
        w_.writeheader(); w_.writerows(rows)
    print(f'\nwrote {len(rows)} wallets -> {OUT}\n')
    mk = [r for r in rows if r['maker_share'] >= 0.6]
    print('TOP MAKER-DOMINANT WALLETS BY REALISED P&L')
    print('  %-44s %10s %7s %6s %6s %s' % ('wallet', 'pnl', 'fills', 'mk%', 'evts', 'legs/evt'))
    for r in mk[:12]:
        print('  %-44s %10.0f %7d %6.2f %6d %.2f'
              % (r['wallet'], r['pnl'], r['fills'], r['maker_share'],
                 r['events'], r['avg_legs_per_event']))
    if mk:
        prof = [r for r in mk if r['pnl'] > 0]
        print('\n  maker-dominant wallets: %d   profitable: %d (%.0f%%)'
              % (len(mk), len(prof), 100 * len(prof) / len(mk)))
        if prof:
            print('  THE QUESTION: legs quoted per match, profitable makers')
            a = sum(r['avg_legs_per_event'] for r in prof) / len(prof)
            s3 = sum(r['all3_event_share'] for r in prof) / len(prof)
            print('    mean legs/match      : %.2f  (3.0 = full book like ours)' % a)
            print('    share of matches all3: %.2f' % s3)
    return 0


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