mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
1bfc91dd6a
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>
358 lines
14 KiB
Python
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()
|