Files
winning-wallet-finder_github/research/surgebot.py
T
jaxperro 1bfc91dd6a surgebot: read-only /feed endpoint for the /surge dashboard
CORS-open GET /feed serving the paper book (equity, counters, open,
settled, skips, informed-set meta) — paper data only, nothing mutable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 15:29:12 -04:00

358 lines
14 KiB
Python

#!/usr/bin/env python3
"""surgebot — Study A's surge-momentum signal as a real-time PAPER harness.
PAPER ONLY. No keys, no orders, no bot imports — this file is self-contained
(recorder-style: baked into the wwf-surgebot image, must not depend on the
repo to boot). It exists to answer the two questions the tape sim cannot:
does the signal compute in real time on the live stream, and what does the
paper book do at the $100/5%-stake deployment spec (#16 sprint plan). Its
entries are graded nightly against chain truth on the Mac (grade_surge.py);
believing ANY of it is gated on the pre-registered #16 forward verdict.
FROZEN signal (params/study_flow.json, 2026-07-20 — do not tune here):
informed set top-150 (fetched from the repo, regenerated nightly)
trigger net informed flow >= $300 in 60s, one market
band entry price 0.10-0.90 · niches sports+esports
cooldown 900s per token
Deployment spec (2026-07-21 sizing discussion):
bank $100 paper · stake 5% of equity, set once per UTC day, $1 floor
cash-gated all-or-nothing · max 2 open positions per real-world event
Paper fill = live CLOB best ask inside p_ref*1.05, else crater (the same
FAK model the copybot's paper mode uses); venue fee 3% * shares * min(p,1-p).
Settles provisionally from the CLOB closed/winner flags; the nightly
re-grades with CTF payout vectors (operator-resolved markets lie here).
"""
import json
import os
import re
import ssl
import threading
import time
import urllib.request
import websocket
WS_URL = "wss://ws-live-data.polymarket.com"
CLOB = "https://clob.polymarket.com"
SET_URL = ("https://raw.githubusercontent.com/jaxperro/winning-wallet-finder/"
"main/research/params/informed_set.json")
STATE = os.environ.get("SURGE_STATE", "/data/surge_state.json")
BANK = 100.0
STAKE_PCT = 0.05
EVENT_CAP = 2
FLOW_USD = 300.0
WINDOW_S = 60
BAND = (0.10, 0.90)
COOLDOWN_S = 900
FEE_RATE = 0.03
SLIP_CAP = 0.05
NICHES = {"sports", "esports"}
NICHE_PATTERNS = [
("esports", ["lol:", "dota", "cs2", "csgo", "valorant", "esports",
"bilibili", "map ", "game 1", "game 2", "game 3"]),
("tennis", ["tennis", "atp", "wta", "wimbledon", "set winner"]),
("sports", [" vs. ", " vs ", " @ ", "mlb", "nba", "nhl", "ufc",
"world cup", "f1", "grand prix", "fifa"]),
]
SSL_CTX = ssl._create_unverified_context()
def log(m):
print(f"{time.strftime('%H:%M:%S')} {m}", flush=True)
def niche(title):
t = (title or "").lower()
for label, pats in NICHE_PATTERNS:
if any(p in t for p in pats):
return label
return "other"
def event_key(slug):
m = re.match(r"(.*?\d{4}-\d{2}-\d{2})", slug or "")
return m.group(1) if m else (slug or None)
def get_json(url, timeout=8):
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
with urllib.request.urlopen(req, timeout=timeout, context=SSL_CTX) as r:
return json.loads(r.read().decode())
class Surge:
def __init__(self):
self.state = {"cash": BANK, "day": "", "day_stake": 5.0,
"open": {}, "settled": [], "skips": [],
"counters": {"triggers": 0, "fills": 0, "craters": 0,
"cash_skip": 0, "event_skip": 0}}
if os.path.exists(STATE):
try:
self.state = json.load(open(STATE))
log(f"resumed: cash ${self.state['cash']:.2f} · "
f"{len(self.state['open'])} open")
except Exception as e:
log(f"⚠ state load failed ({e}) — fresh book")
self.informed = set()
self.set_meta = {}
self.win = {} # asset -> [(ts, ±usd)]
self.last_trig = {}
self.lock = threading.Lock()
self._roll_day(force=True)
# ── config / sizing ────────────────────────────────────────────────────
def load_set(self):
try:
d = get_json(SET_URL, timeout=15)
self.informed = {w.lower() for w in d["wallets"]}
self.set_meta = {"generated_at": d.get("generated_at"),
"n": len(self.informed)}
age_h = (time.time() - (d.get("generated_at") or 0)) / 3600
log(f"informed set: {len(self.informed)} wallets "
f"({age_h:.0f}h old)" + (" ⚠ STALE >48h" if age_h > 48 else ""))
except Exception as e:
log(f"⚠ informed set fetch failed ({e}) — "
f"keeping {len(self.informed)} cached")
def equity(self):
return self.state["cash"] + sum(p["cost"] for p in
self.state["open"].values())
def _roll_day(self, force=False):
day = time.strftime("%Y-%m-%d", time.gmtime())
if force or day != self.state["day"]:
self.state["day"] = day
self.state["day_stake"] = max(1.0, round(STAKE_PCT * self.equity(), 2))
log(f"day {day}: stake ${self.state['day_stake']:.2f} "
f"(5% of ${self.equity():.2f} equity)")
def persist(self):
tmp = STATE + ".tmp"
json.dump(self.state, open(tmp, "w"))
os.replace(tmp, STATE)
# ── signal ─────────────────────────────────────────────────────────────
def on_trade(self, p):
w = (p.get("proxyWallet") or "").lower()
if w not in self.informed:
return
title = p.get("title") or ""
if niche(title) not in NICHES:
return
asset = p.get("asset")
px = float(p.get("price") or 0)
usd = px * float(p.get("size") or 0)
if p.get("side") == "SELL":
usd = -usd
now = time.time()
with self.lock:
buf = self.win.setdefault(asset, [])
buf.append((now, usd))
while buf and buf[0][0] < now - WINDOW_S:
buf.pop(0)
flow = sum(u for _, u in buf)
if flow < FLOW_USD or not (BAND[0] <= px <= BAND[1]):
return
if now - self.last_trig.get(asset, 0) < COOLDOWN_S:
return
self.last_trig[asset] = now
self.state["counters"]["triggers"] += 1
self.execute(p, asset, px, flow, title)
# ── paper execution (called under lock) ────────────────────────────────
def execute(self, p, asset, p_ref, flow, title):
self._roll_day()
c = self.state["counters"]
ev = event_key(p.get("eventSlug") or p.get("slug") or "")
if asset in self.state["open"]:
return
n_ev = sum(1 for o in self.state["open"].values() if o["event"] == ev)
if ev and n_ev >= EVENT_CAP:
c["event_skip"] += 1
log(f"skip (event cap) {title[:40]}")
return
stake = self.state["day_stake"]
if self.state["cash"] < stake:
c["cash_skip"] += 1
log(f"skip (no cash: ${self.state['cash']:.2f}) {title[:40]}")
return
try:
book = get_json(f"{CLOB}/book?token_id={asset}")
asks = [float(a["price"]) for a in (book.get("asks") or [])]
ba = min(asks) if asks else None
except Exception as e:
log(f"skip (book fetch failed: {e}) {title[:40]}")
return
cap = min(p_ref * (1 + SLIP_CAP), 0.99)
if ba is None or ba > cap:
c["craters"] += 1
self.state["skips"].append({"ts": int(time.time()), "asset": asset,
"p_ref": p_ref, "ba": ba, "flow": flow,
"title": title[:60], "why": "crater"})
del self.state["skips"][:-500]
log(f"CRATER {title[:40]} ref {p_ref:.2f} ask "
f"{('%.2f' % ba) if ba else '—'}")
self.persist()
return
shares = stake / ba
fee = FEE_RATE * shares * min(ba, 1 - ba)
self.state["cash"] -= stake + fee
self.state["open"][asset] = {
"ts": int(time.time()), "cond": p.get("conditionId"),
"event": ev, "title": title[:60],
"outcome": p.get("outcome"), "p_ref": p_ref, "price": ba,
"shares": round(shares, 4), "cost": stake, "fee": round(fee, 4),
"flow": round(flow)}
c["fills"] += 1
log(f"FILL {p.get('outcome')} · {title[:40]} @ {ba:.3f} "
f"(${stake:.2f}, flow ${flow:.0f}) · cash ${self.state['cash']:.2f}")
self.persist()
# ── provisional settles (nightly re-grades with chain truth) ───────────
def settle_pass(self):
for asset, pos in list(self.state["open"].items()):
try:
m = get_json(f"{CLOB}/markets/{pos['cond']}")
except Exception:
continue
if not m.get("closed"):
continue
pay = None
for t in m.get("tokens") or []:
if str(t.get("token_id")) == str(asset):
pay = 1.0 if t.get("winner") else 0.0
if pay is None:
continue
self.state["cash"] += pos["shares"] * pay
self.state["settled"].append({**pos, "asset": asset, "payout": pay,
"provisional": True,
"settled_ts": int(time.time()),
"pnl": round(pos["shares"] * pay
- pos["cost"] - pos["fee"], 2)})
del self.state["open"][asset]
log(f"SETTLE {'WON' if pay else 'lost'} {pos['title'][:38]} "
f"{'+' if pay else ''}{pos['shares'] * pay - pos['cost'] - pos['fee']:.2f} "
f"· cash ${self.state['cash']:.2f}")
self.persist()
def run_conn(tag, bot):
backoff = 2
while True:
state = {"fresh": time.time()}
def on_open(ws):
ws.send(json.dumps({"action": "subscribe", "subscriptions":
[{"topic": "activity", "type": "trades",
"filters": ""}]}))
state["fresh"] = time.time()
log(f"ws[{tag}]: connected")
def ping():
while ws.keep_running:
time.sleep(5)
try:
ws.send('{"action":"ping"}')
except Exception:
break
if time.time() - state["fresh"] > 20:
try:
ws.close()
except Exception:
pass
break
threading.Thread(target=ping, daemon=True).start()
def on_message(ws, raw):
state["fresh"] = time.time()
try:
m = json.loads(raw)
except Exception:
return
if m.get("topic") == "activity" and m.get("type") == "trades":
try:
bot.on_trade(m.get("payload") or {})
except Exception as e:
log(f"⚠ on_trade error: {e}")
try:
app = websocket.WebSocketApp(WS_URL, on_open=on_open,
on_message=on_message)
app.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
except Exception as e:
log(f"ws[{tag}]: {str(e)[:60]}")
time.sleep(backoff + (1 if tag == "b" else 0))
backoff = min(backoff * 2, 30)
if time.time() - state["fresh"] < 60:
backoff = 2
def serve_feed(bot, port=8080):
"""Read-only public feed for the /surge dashboard (paper data only —
nothing here can place an order or mutate state). CORS-open."""
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class H(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def do_GET(self):
if self.path.split("?")[0] != "/feed":
self.send_response(404)
self.end_headers()
return
with bot.lock:
st = bot.state
body = json.dumps({
"mode": "paper", "updated": int(time.time()),
"bank": BANK, "equity": round(bot.equity(), 2),
"cash": round(st["cash"], 2),
"day": st["day"], "day_stake": st["day_stake"],
"stake_pct": STAKE_PCT, "event_cap": EVENT_CAP,
"flow_usd": FLOW_USD, "window_s": WINDOW_S,
"band": BAND, "counters": st["counters"],
"informed": bot.set_meta,
"open": [{**p, "asset": a} for a, p in st["open"].items()],
"settled": st["settled"][-200:],
"skips": st["skips"][-100:]}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
ThreadingHTTPServer(("0.0.0.0", port), H).serve_forever()
def main():
bot = Surge()
bot.load_set()
threading.Thread(target=serve_feed, args=(bot,), daemon=True).start()
for tag in ("a", "b"):
threading.Thread(target=run_conn, args=(tag, bot), daemon=True).start()
last_set = time.time()
n = 0
while True:
time.sleep(60)
n += 1
with bot.lock:
bot._roll_day()
if n % 2 == 0:
with bot.lock:
bot.settle_pass()
if time.time() - last_set > 6 * 3600:
bot.load_set()
last_set = time.time()
c = bot.state["counters"]
log(f"paper ${bot.equity():.2f} (cash ${bot.state['cash']:.2f}) · "
f"open {len(bot.state['open'])} · trig {c['triggers']} "
f"fill {c['fills']} crater {c['craters']} "
f"skip {c['cash_skip']}c/{c['event_skip']}e")
if __name__ == "__main__":
main()