"""What do the SUCCESSFUL Polymarket soccer makers actually do differently?

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

The coarse cuts came back empty: among genuine two-sided makers, winners and
losers are indistinguishable on legs quoted (2.08 vs 2.16), full-book rate
(42% vs 44%), two-sidedness (0.73 vs 0.72) and inventory discipline (0.17 vs
0.13). The only separator was SIZE, and in the wrong direction -- losers traded
2.3x the volume.

So this looks at the things a maker actually controls, per fill:

  ADVERSE SELECTION -- the core maker risk and the one our own xlow verdict is
  built on. For each fill, where did the price go afterwards? Signed by side:
  a maker who SOLD at 0.40 and sees 0.30 ten minutes later was right; one who
  sees 0.55 was picked off. Mid is estimated as the volume-weighted price of
  trades in a window around each timestamp, since the resting book is not
  public.

  TIMING -- share of volume in the first vs last third of the market's life.
  Our own decile study says the money is in a market's first ~30% of volume,
  so if that transfers, winners should be early.

  PRICE BUCKET -- do they make on favourites, longshots, or the draw? The draw
  is the leg our maker strands on.

  HOLD vs FLATTEN -- what fraction of their peak position is still open at
  settlement. This is the strand question in the only form this data can
  answer.

EVERYTHING IS FILLS, NEVER QUOTES: Polymarket's CLOB is off-chain, so resting
orders are public nowhere. A maker who quoted and was not hit is invisible.
"""
import bisect
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__))
CACHE = os.path.join(HERE, '_pm_trades_cache.json')
OUT = os.path.join(HERE, 'pm_maker_behaviour.csv')
PAGE, OFFSET_CAP = 500, 10000
HORIZONS = (600, 1800, 7200)          # seconds after a fill


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.08)
    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
            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 load(n_matches):
    if os.path.exists(CACHE):
        with open(CACHE, encoding='utf-8') as fh:
            d = json.load(fh)
        if d.get('n') == n_matches:
            print(f'using cached tape ({sum(len(v["all"]) for v in d["mk"].values())} rows)')
            return d['mk']
    evs = complete_matches(n_matches)
    mk = {}
    for e in evs:
        t = (e.get('title') or '').encode('ascii', 'replace').decode()
        print(f'  {t[:50]:<50}', end='', flush=True)
        nr = 0
        for m in e['markets']:
            cid = m.get('conditionId')
            allr = trades(cid, False)
            tk = [f"{x['transactionHash']}|{x['proxyWallet']}" for x in trades(cid, True)]
            try:
                pr = [float(x) for x in json.loads(m.get('outcomePrices'))]
            except Exception:
                pr = []
            mk[cid] = {'all': allr, 'takers': tk, 'prices': pr,
                       'slug': e.get('slug'), 'q': m.get('question')}
            nr += len(allr)
        print(f' {nr:>6} rows')
    with open(CACHE, 'w', encoding='utf-8') as fh:
        json.dump({'n': n_matches, 'mk': mk}, fh)
    return mk


def main():
    n_matches = int(sys.argv[1]) if len(sys.argv) > 1 else 10
    mk = load(n_matches)

    W = collections.defaultdict(lambda: {
        'drift': {h: [] for h in HORIZONS}, 'px': [], 'cash': 0.0,
        'pos': collections.defaultdict(float), 'peak': collections.defaultdict(float),
        'settle': {}, 'life_frac': [], 'n': 0, 'buy': collections.defaultdict(float),
        'sell': collections.defaultdict(float)})

    for cid, d in mk.items():
        allr, tset = d['all'], set(d['takers'])
        prices = d['prices']
        # tape per outcome token, sorted by time, for the drift reference
        tape = collections.defaultdict(list)
        for t in allr:
            try:
                tape[int(t.get('outcomeIndex') or 0)].append(
                    (int(t['timestamp']), float(t['price']), float(t['size'])))
            except (TypeError, ValueError):
                pass
        for k in tape:
            tape[k].sort()
        times = {k: [x[0] for x in v] for k, v in tape.items()}
        if not times:
            continue
        t_lo = min(min(v) for v in times.values() if v)
        t_hi = max(max(v) for v in times.values() if v)
        span = max(t_hi - t_lo, 1)

        def vwap(oi, t0, t1):
            v = tape[oi]; ts = times[oi]
            i, j = bisect.bisect_left(ts, t0), bisect.bisect_right(ts, t1)
            seg = v[i:j]
            if not seg:
                return None
            num = sum(p * s for _, p, s in seg); den = sum(s for _, _, s in seg)
            return num / den if den else None

        for t in allr:
            w = t['proxyWallet']
            if f"{t['transactionHash']}|{w}" in tset:
                continue                      # taker row
            try:
                ts_ = int(t['timestamp']); px = float(t['price'])
                sz = float(t['size']); oi = int(t.get('outcomeIndex') or 0)
            except (TypeError, ValueError):
                continue
            s = W[w]; s['n'] += 1
            s['px'].append(px)
            s['life_frac'].append((ts_ - t_lo) / span)
            side = (t.get('side') or '').upper()
            sign = -1.0 if side == 'BUY' else 1.0      # maker sold -> +cash
            s['cash'] += sign * sz * px
            s['pos'][(cid, oi)] += (sz if side == 'BUY' else -sz)
            s['peak'][(cid, oi)] = max(s['peak'][(cid, oi)],
                                       abs(s['pos'][(cid, oi)]))
            s['settle'][(cid, oi)] = prices[oi] if oi < len(prices) else 0.0
            (s['buy'] if side == 'BUY' else s['sell'])[(cid, oi)] += sz
            for h in HORIZONS:
                fut = vwap(oi, ts_ + h // 2, ts_ + h * 2)
                if fut is None:
                    continue
                # signed so POSITIVE = the market moved the maker's way
                s['drift'][h].append((fut - px) * (1.0 if side == 'BUY' else -1.0))

    rows = []
    for w, s in W.items():
        if s['n'] < 100:
            continue
        keys = set(s['buy']) | set(s['sell'])
        flips = []
        gross = 0.0
        for k in keys:
            b, sl = s['buy'].get(k, 0.0), s['sell'].get(k, 0.0)
            gross += b + sl
            flips.append(min(b, sl) / max(b, sl) if max(b, sl) else 0.0)
        flip = sum(flips) / len(flips) if flips else 0.0
        netexp = sum(abs(s['buy'].get(k, 0) - s['sell'].get(k, 0)) for k in keys) / gross if gross else 1.0
        if flip < 0.3 or netexp > 0.6:
            continue                       # not a genuine two-sided maker
        settle = sum(q * s['settle'].get(k, 0.0) for k, q in s['pos'].items())
        held = sum(abs(q) for q in s['pos'].values())
        peak = sum(s['peak'].values()) or 1.0
        r = {'wallet': w, 'pnl': round(s['cash'] + settle, 2), 'fills': s['n'],
             'flip': round(flip, 3), 'net_exposure': round(netexp, 3),
             'gross_volume': round(gross),
             'mean_fill_px': round(sum(s['px']) / len(s['px']), 3),
             'early_share': round(sum(1 for f in s['life_frac'] if f < 0.33) / len(s['life_frac']), 3),
             'late_share': round(sum(1 for f in s['life_frac'] if f > 0.67) / len(s['life_frac']), 3),
             'held_to_settle': round(held / peak, 3)}
        for h in HORIZONS:
            v = s['drift'][h]
            r[f'drift_{h//60}m'] = round(sum(v) / len(v), 5) if v else None
        rows.append(r)

    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)} true makers -> {OUT}\n')

    win = [r for r in rows if r['pnl'] > 0]
    los = [r for r in rows if r['pnl'] <= 0]
    def avg(rs, k):
        v = [r[k] for r in rs if r.get(k) is not None]
        return sum(v) / len(v) if v else float('nan')
    print('BEHAVIOUR OF WINNERS vs LOSERS  (true two-sided makers)')
    print('  %-22s %12s %12s' % ('', 'WINNERS n=%d' % len(win), 'LOSERS n=%d' % len(los)))
    for k, lab in (('drift_10m', 'drift +10m (c)'), ('drift_30m', 'drift +30m (c)'),
                   ('drift_120m', 'drift +120m (c)'), ('mean_fill_px', 'mean fill price'),
                   ('early_share', 'share filled early'), ('late_share', 'share filled late'),
                   ('held_to_settle', 'held to settlement'), ('flip', 'two-sidedness'),
                   ('net_exposure', 'net exposure'), ('gross_volume', 'gross volume')):
        a, b = avg(win, k), avg(los, k)
        if k.startswith('drift'):
            a, b = a * 100, b * 100
        print('  %-22s %12.3f %12.3f' % (lab, a, b))
    return 0


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