#!/usr/bin/env python3 """RTDS tape recorder — every Polymarket trade, hour-rotated gzip segments. SILO (2026-07-17, user directive): own Fly app (wwf-recorder) + volume, no imports from any bot, no shared state — a recorder crash can never touch trading and a bot deploy can never gap the tape. Socket handling copies the battle-tested RtdsListener patterns (unfiltered subscribe — server-side filters silently return nothing; app-level ping; 120s silent-stale force reconnect; capped backoff). One JSON line per trade: {ts, wallet, asset, cond, side, price, size, tx, title} Current hour writes plain to /data/segments/rtds_YYYYMMDD_HH.jsonl; rotation gzips it. A disk guard deletes oldest segments past 85% volume use. The Mac's nightly ingest (recorder/ingest.py via daily.sh) pulls closed segments into live/rtds.duckdb and deletes them here after verified insert.""" import gzip import json import os import shutil import ssl import threading import time import websocket URL = "wss://ws-live-data.polymarket.com" DIR = os.environ.get("TAPE_DIR", "/data/segments") SUB = json.dumps({"action": "subscribe", "subscriptions": [ {"topic": "activity", "type": "trades", "filters": ""}]}) def log(m): print(f"{time.strftime('%H:%M:%S')} {m}", flush=True) class Tape: def __init__(self): os.makedirs(DIR, exist_ok=True) self.hour, self.fh, self.n = None, None, 0 self.msgs = self.gaps = 0 self.last_msg = time.time() def _rotate(self, hour): if self.fh: self.fh.close() plain = os.path.join(DIR, f"rtds_{self.hour}.jsonl") with open(plain, "rb") as i, gzip.open(plain + ".gz", "wb") as o: shutil.copyfileobj(i, o) os.remove(plain) log(f"rotated {self.hour} ({self.n} rows)") self.hour, self.n = hour, 0 self.fh = open(os.path.join(DIR, f"rtds_{hour}.jsonl"), "a") # disk guard: drop oldest closed segments past 85% usage try: st = os.statvfs(DIR) while st.f_bavail / st.f_blocks < 0.15: old = sorted(f for f in os.listdir(DIR) if f.endswith(".gz")) if not old: break os.remove(os.path.join(DIR, old[0])) log(f"disk guard dropped {old[0]}") st = os.statvfs(DIR) except Exception: pass def write(self, p): hour = time.strftime("%Y%m%d_%H", time.gmtime()) if hour != self.hour: self._rotate(hour) ts = p.get("timestamp") or 0 if ts > 1e12: ts /= 1000.0 self.fh.write(json.dumps({ "ts": round(ts or time.time(), 3), "wallet": (p.get("proxyWallet") or "").lower(), "asset": p.get("asset"), "cond": p.get("conditionId"), "side": p.get("side"), "price": p.get("price"), "size": p.get("size"), "tx": p.get("transactionHash"), "title": str(p.get("title") or "")[:60]}) + "\n") self.n += 1 self.msgs += 1 self.last_msg = time.time() def main(): tape = Tape() def hb(): last = 0 while True: time.sleep(60) log(f"tape: {tape.msgs - last} msg/min · hour rows {tape.n} · gaps {tape.gaps}") last = tape.msgs threading.Thread(target=hb, daemon=True).start() backoff = 2 while True: def on_open(ws): ws.send(SUB) tape.last_msg = time.time() # fresh clock — a stale one from the # PREVIOUS connection would trip the guard instantly and loop log("rtds: connected — recording unfiltered trades") def ping(): while ws.keep_running: time.sleep(5) try: ws.send('{"action":"ping"}') except Exception: break if time.time() - tape.last_msg > 45: log("rtds: silent 45s — forcing reconnect") tape.gaps += 1 try: ws.close() except Exception: pass break threading.Thread(target=ping, daemon=True).start() def on_message(ws, raw): try: m = json.loads(raw) except Exception: return if m.get("topic") == "activity" and m.get("type") == "trades": tape.write(m.get("payload") or {}) try: app = websocket.WebSocketApp(URL, on_open=on_open, on_message=on_message) app.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}) backoff = 2 except Exception as e: log(f"listener error {str(e)[:70]}") tape.gaps += 1 log(f"stream down — reconnect in {backoff}s") time.sleep(backoff) backoff = min(backoff * 2, 60) if __name__ == "__main__": main()