"""Separate real market makers from patient directional traders, then rank them.

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

THE PROBLEM THIS SOLVES. `maker_share` only means "the order was resting when it
got hit", which a whale building a position satisfies exactly as well as a
market maker. That contaminated the earlier ranking: the top wallets by P&L were
1.00 legs / 0% all-three and made seven figures on a single match, which is a
directional bet, not making.

THE DISCRIMINATOR. A market maker works BOTH SIDES of the same token -- buying
and selling it repeatedly and carrying little net exposure. A directional trader
only ever accumulates. So per wallet, per token:

    flip     = min(buy_qty, sell_qty) / max(buy_qty, sell_qty)   ~1 maker, ~0 directional
    net_exp  = |net position| / gross volume                      ~0 maker, ~1 directional

P&L is settlement-aware: cash flow plus net position valued at gamma
outcomePrices. That is robust to SPLIT/MERGE even though those never appear as
trades -- a split's $1 cost equals the settlement liability of the phantom
-1 Yes / -1 No position it creates, because p_yes + p_no = 1. (I previously
claimed split/merge broke this accounting. It does not.)

ONLY COMPLETE-HISTORY MATCHES are used: /trades refuses offset > ~10500, and a
truncated market returns its most recent rows -- a settlement-adjacent slice of
0.999/0.001 cleanup that would make everyone look one-sided and profitable.

STILL FILLS, NEVER QUOTES. Polymarket's CLOB is off-chain, so resting orders are
public nowhere. "legs" means legs FILLED.
"""
import collections
import csv
import json
import os
import sys
import time

import requests

GAMMA = 'https://gamma-api.polymarket.com'
DATA = 'https://data-api.polymarket.com'
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, 'pm_maker_twosided.csv')
PAGE = 500
OFFSET_CAP = 10000


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.2 * (i + 1)); continue
        if r.status_code == 200:
            return r.json()
        if r.status_code == 400:
            return None
        time.sleep(1.2 * (i + 1))
    return None


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.1)
    return rows


def complete_matches(n):
    out = []
    for off in (0, 100, 200, 300, 400):
        evs = _get(f'{GAMMA}/events',
                   {'limit': 100, 'offset': off, 'tag_slug': 'soccer',
                    'closed': 'true', 'order': 'volume', 'ascending': 'false'})
        if not evs:
            break
        for e in evs:
            ms = e.get('markets') or []
            if len(ms) != 3 or ' vs' not in (e.get('title') or '').lower():
                continue
            if not all(m.get('outcomePrices') for m in ms):
                continue
            # complete history only
            if any(_get(f'{DATA}/trades',
                        {'market': m.get('conditionId'), 'limit': 1,
                         'offset': OFFSET_CAP - 200, 'takerOnly': 'false'})
                   for m in ms):
                continue
            out.append(e)
            if len(out) >= n:
                return out
    return out


def main():
    n_matches = int(sys.argv[1]) if len(sys.argv) > 1 else 10
    evs = complete_matches(n_matches)
    print(f'{len(evs)} complete-history soccer matches\n')

    W = collections.defaultdict(lambda: {
        'buy': collections.defaultdict(float), 'sell': collections.defaultdict(float),
        'cash': 0.0, 'pos': collections.defaultdict(float),
        'settle_px': {}, 'mk': 0, 'tk': 0,
        'legs': collections.defaultdict(set), 'matches': set()})

    for e in evs:
        title = (e.get('title') or '').encode('ascii', 'replace').decode()
        print(f'  {title[:50]:<50}', end='', flush=True)
        nr = 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 = trades(cid, False)
            tk = {(t['transactionHash'], t['proxyWallet']) for t in trades(cid, True)}
            nr += len(allr)
            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
                key = (cid, oi)
                s = W[w]
                if (t['transactionHash'], w) in tk:
                    s['tk'] += 1
                else:
                    s['mk'] += 1
                s['matches'].add(e.get('slug'))
                s['legs'][e.get('slug')].add(cid)
                s['settle_px'][key] = prices[oi] if oi < len(prices) else 0.0
                if (t.get('side') or '').upper() == 'BUY':
                    s['buy'][key] += sz; s['cash'] -= sz * px; s['pos'][key] += sz
                else:
                    s['sell'][key] += sz; s['cash'] += sz * px; s['pos'][key] -= sz
        print(f' {nr:>6} rows')

    rows = []
    for w, s in W.items():
        tot = s['mk'] + s['tk']
        if tot < 100 or s['mk'] / tot < 0.6:
            continue
        keys = set(s['buy']) | set(s['sell'])
        flips, nets, gross = [], [], 0.0
        two_sided = 0
        for k in keys:
            b, sl = s['buy'].get(k, 0.0), s['sell'].get(k, 0.0)
            g = b + sl
            gross += g
            if b > 0 and sl > 0:
                two_sided += 1
            flips.append(min(b, sl) / max(b, sl) if max(b, sl) > 0 else 0.0)
            nets.append(abs(b - sl))
        settle = sum(q * s['settle_px'].get(k, 0.0) for k, q in s['pos'].items())
        nlegs = [len(v) for v in s['legs'].values()]
        rows.append({
            'wallet': w,
            'pnl': round(s['cash'] + settle, 2),
            'fills': tot,
            'maker_share': round(s['mk'] / tot, 3),
            'tokens': len(keys),
            'two_sided_share': round(two_sided / len(keys), 3) if keys else 0,
            'flip': round(sum(flips) / len(flips), 3) if flips else 0,
            'net_exposure': round(sum(nets) / gross, 3) if gross else 1.0,
            'gross_volume': round(gross),
            'matches': len(s['matches']),
            'mean_legs': round(sum(nlegs) / len(nlegs), 2) if nlegs else 0,
            'all3_share': round(sum(1 for x in nlegs if x == 3) / len(nlegs), 2) if nlegs else 0,
        })
    rows.sort(key=lambda r: -r['pnl'])
    with open(OUT, 'w', newline='', encoding='utf-8') as fh:
        wtr = csv.DictWriter(fh, fieldnames=list(rows[0].keys()))
        wtr.writeheader(); wtr.writerows(rows)
    print(f'\nwrote {len(rows)} wallets -> {OUT}\n')

    # A REAL maker works both sides and carries little net exposure.
    real = [r for r in rows if r['flip'] >= 0.3 and r['net_exposure'] <= 0.6]
    dirn = [r for r in rows if r['flip'] < 0.1]
    def summarise(rs, label):
        if not rs:
            print(f'  {label}: none'); return
        p = [r for r in rs if r['pnl'] > 0]
        med = sorted(r['pnl'] for r in rs)[len(rs) // 2]
        print('  %-26s n=%-4d profitable=%3.0f%%  median pnl=%9.0f  legs=%.2f all3=%.0f%%'
              % (label, len(rs), 100 * len(p) / len(rs), med,
                 sum(r['mean_legs'] for r in rs) / len(rs),
                 100 * sum(r['all3_share'] for r in rs) / len(rs)))
    print('POPULATION SPLIT')
    summarise(rows, 'all maker-dominant')
    summarise(real, 'TRUE MAKERS (2-sided)')
    summarise(dirn, 'directional (1-sided)')

    if real:
        print('\nTRUE MAKERS BY P&L')
        print('  %-44s %10s %6s %5s %6s %5s %s' %
              ('wallet', 'pnl', 'fills', 'flip', 'netexp', 'legs', 'all3'))
        for r in sorted(real, key=lambda r: -r['pnl'])[:12]:
            print('  %-44s %10.0f %6d %5.2f %6.2f %5.2f %4.0f%%'
                  % (r['wallet'], r['pnl'], r['fills'], r['flip'],
                     r['net_exposure'], r['mean_legs'], 100 * r['all3_share']))
        win = [r for r in real if r['pnl'] > 0]
        los = [r for r in real if r['pnl'] <= 0]
        print('\nWHAT SEPARATES WINNERS FROM LOSERS (true makers only)')
        for label, rs in (('winners', win), ('losers', los)):
            if rs:
                print('  %-8s n=%-3d flip=%.2f netexp=%.2f legs=%.2f all3=%.0f%% gross=%.0f'
                      % (label, len(rs),
                         sum(r['flip'] for r in rs) / len(rs),
                         sum(r['net_exposure'] for r in rs) / len(rs),
                         sum(r['mean_legs'] for r in rs) / len(rs),
                         100 * sum(r['all3_share'] for r in rs) / len(rs),
                         sum(r['gross_volume'] for r in rs) / len(rs)))
    return 0


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