mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
e6730ef579
- surgebot/oraclebot: settles now append-only to /data/*_settles.jsonl (SETTLED_TRIM rotation can never lose a settle; graders pull them) - forward.py: backfills any tape-covered day missing from the ledger — a Mac offline gap > RESCORE_DAYS no longer leaves verdict-evidence holes - meta_snap.py: nightly gzipped snapshot of all active markets (12k, 2MB/d, local-only) — end dates make tau knowable at trigger for every tape trigger; token->outcome maps kill the label-gap scorer artifact class - .gitignore: pulled raw streams + meta stay local (re-fetchable, lean repo) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
596 lines
25 KiB
Python
596 lines
25 KiB
Python
#!/usr/bin/env python3
|
||
"""surgebot A2 — Study A's surge signal as a real-time MEASUREMENT harness.
|
||
|
||
PAPER ONLY. No keys, no orders, no bot imports — self-contained (baked into
|
||
the wwf-surgebot image, must not depend on the repo to boot).
|
||
|
||
WHY A2 (2026-07-22 post-mortem of the v1 paper book): v1 rehearsed the
|
||
$100/5% deployment spec physically — cash-gating meant it attempted ~2% of
|
||
triggers, and that subsample was ADVERSELY SELECTED (its 102 chain-graded
|
||
fills: −$9/fill at $100-scale, while the sim scored ALL of the same day's
|
||
triggers at +$41/fill; the sim run on v1's own triggers agreed with v1's
|
||
losses within ~$2 — execution physics validated, sampling condemned).
|
||
A cash-gated book cannot measure the signal AND rehearse the spec at once.
|
||
|
||
A2 splits them: this box attempts EVERY cooldown-passed trigger (flat $100
|
||
paper FAK walking the live asks inside p_ref*1.05, partial fills kept) and
|
||
appends one line per attempt — including top-5 raw ask levels — to
|
||
/data/surge_attempts.jsonl. Any bankroll spec (the $100/5% book, other
|
||
banks, caps, tighter slip) is then replayed OFFLINE from that stream
|
||
(research/surge_book_replay.py -> research/surge_book.json, nightly).
|
||
|
||
FROZEN signal semantics (params/study_flow.json — 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, set at trigger time
|
||
Unchanged from v1: signal code path is verbatim. Changed in A2 (execution/
|
||
accounting only): every-trigger attempts (no cash gate, no event cap, no
|
||
skip-if-open — those live in the offline replay), $100 book-walk fills
|
||
(ledger-comparable; v1 was best-ask at ~$5), attempts log, multi-lot per
|
||
asset. THE #16 VERDICT BINDS TO forward_ledger.jsonl ONLY; this harness
|
||
measures capturability. Settles are provisional (CLOB closed/winner) until
|
||
the nightly CTF payout re-grade (grade_surge.py -> surge_meas_ledger.jsonl;
|
||
v1's surge_paper_ledger.jsonl is closed and untouched, as is its state at
|
||
/data/surge_state.json). Any semantics-altering fix bumps SEM_VER and
|
||
restarts the shakedown clock.
|
||
|
||
OBSERVATIONAL instrumentation (2026-07-22, NOT semantics — SEM_VER
|
||
unchanged): attempts also record top-3 bids (spread context; exit-fill
|
||
realism), and every fill schedules book re-reads at +60s/+300s/+1800s whose
|
||
best bid/ask land in /data/surge_markouts.jsonl — real exit marks for the
|
||
markout-exit study instead of print-inferred guesses. Pending re-reads are
|
||
lost on restart (counted, acceptable — observation only).
|
||
"""
|
||
import heapq
|
||
import json
|
||
import os
|
||
import queue
|
||
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/surge2_state.json")
|
||
ATTEMPTS = os.environ.get("SURGE_ATTEMPTS", "/data/surge_attempts.jsonl")
|
||
MARKOUTS = os.environ.get("SURGE_MARKOUTS", "/data/surge_markouts.jsonl")
|
||
SETTLES = os.environ.get("SURGE_SETTLES", "/data/surge_settles.jsonl")
|
||
SEM_VER = "a2" # bump on ANY semantics-altering change
|
||
MARKOUT_OFFSETS = (60, 300, 1800) # observational re-reads per fill
|
||
BID_LEVELS = 3
|
||
|
||
# ── FROZEN — must equal params/study_flow.json / forward.py ────────────────
|
||
FLOW_USD = 300.0
|
||
WINDOW_S = 60
|
||
BAND = (0.10, 0.90)
|
||
COOLDOWN_S = 900
|
||
NICHES = {"sports", "esports"}
|
||
STAKE = 100.0 # ledger stake (sf.STAKE) — walk the book
|
||
FEE_RATE = 0.03
|
||
SLIP_CAP = 0.05
|
||
# ── LIVE-ONLY (execution/plumbing, never signal) ───────────────────────────
|
||
BOOK_WORKERS = 4
|
||
BOOK_QUEUE = 16 # overflow => throttle (counted)
|
||
CLOB_PASS_CAP = 40 # settle GETs per pass, round-robin
|
||
SETTLED_TRIM = 10000 # nightly ledger is the durable record
|
||
SKIPS_TRIM = 500
|
||
TOP_LEVELS = 5 # raw ask levels recorded per attempt
|
||
|
||
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())
|
||
|
||
|
||
def iso_ts(s):
|
||
"""CLOB end_date_iso -> epoch (None on any parse trouble)."""
|
||
try:
|
||
import calendar
|
||
return calendar.timegm(time.strptime(
|
||
s.replace("+00:00", "Z")[:20], "%Y-%m-%dT%H:%M:%SZ"))
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def walk_asks(asks, cap, stake_usd=STAKE):
|
||
"""FAK against a CLOB ask list: consume ascending price levels <= cap
|
||
until stake_usd is spent; partial fills kept (that IS what a real FAK
|
||
gets). Returns top raw levels too so ANY smaller stake / tighter cap can
|
||
be replayed offline. asks arrive UNSORTED from /book."""
|
||
lv = sorted(((float(a["price"]), float(a["size"])) for a in asks or []))
|
||
top = [[p, s] for p, s in lv[:TOP_LEVELS]]
|
||
if not lv:
|
||
return {"filled": False, "best_ask": None, "top": top}
|
||
best = lv[0][0]
|
||
spent = shares = 0.0
|
||
levels = 0
|
||
for pxx, sz in lv:
|
||
if pxx > cap or spent >= stake_usd - 1e-9:
|
||
break
|
||
take = min(sz, (stake_usd - spent) / pxx)
|
||
if take <= 0:
|
||
break
|
||
shares += take
|
||
spent += take * pxx
|
||
levels += 1
|
||
if not shares:
|
||
return {"filled": False, "best_ask": best, "top": top}
|
||
return {"filled": True, "vwap": spent / shares, "shares": shares,
|
||
"usd": spent, "levels": levels, "best_ask": best, "top": top,
|
||
"partial": spent < stake_usd - 1e-9}
|
||
|
||
|
||
def fresh_state():
|
||
return {
|
||
"sem_ver": SEM_VER, "boot_ts": int(time.time()),
|
||
"open": {}, "settled": [], "skips": [],
|
||
"pnl_realized": 0.0,
|
||
"counters": {"triggers": 0, "attempts": 0, "fills": 0, "craters": 0,
|
||
"throttle_skip": 0, "book_fail": 0,
|
||
"settled_total": 0, "settle_clob": 0}}
|
||
|
||
|
||
class Surge:
|
||
def __init__(self):
|
||
self.state = fresh_state()
|
||
if os.path.exists(STATE):
|
||
try:
|
||
loaded = json.load(open(STATE))
|
||
if loaded.get("sem_ver") != SEM_VER:
|
||
log(f"⚠ SEM_VER {loaded.get('sem_ver')} -> {SEM_VER} — "
|
||
f"semantics changed; shakedown clock restarts here")
|
||
loaded["sem_ver"] = SEM_VER
|
||
self.state = loaded
|
||
log(f"resumed: pnl {self.state['pnl_realized']:+.2f} · "
|
||
f"{len(self.state['open'])} open · "
|
||
f"{self.state['counters']['fills']} lifetime fills")
|
||
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.book_q = queue.Queue(BOOK_QUEUE)
|
||
self.mo_heap = [] # (due_ts, asset, fill_id, offset)
|
||
|
||
# ── config ─────────────────────────────────────────────────────────────
|
||
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")
|
||
|
||
# ── signal — FROZEN, verbatim v1 path ──────────────────────────────────
|
||
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
|
||
c = self.state["counters"]
|
||
c["triggers"] += 1
|
||
# A2: EVERY trigger becomes an attempt (no cash/event/open gate —
|
||
# bankroll specs are replayed offline from the attempts log)
|
||
job = {"asset": asset, "cond": p.get("conditionId"),
|
||
"event": event_key(p.get("eventSlug") or p.get("slug")
|
||
or ""),
|
||
"title": title[:60], "outcome": p.get("outcome"),
|
||
"niche": niche(title), "ts": round(now, 3),
|
||
"p_ref": px, "flow": round(flow)}
|
||
try:
|
||
self.book_q.put_nowait(job)
|
||
c["attempts"] += 1
|
||
except queue.Full:
|
||
c["throttle_skip"] += 1
|
||
self._skip(job, "throttle", None)
|
||
self.log_attempt({**job, "filled": False, "why": "throttle"})
|
||
|
||
# ── paper execution (workers; network OUT of the lock) ─────────────────
|
||
def book_worker(self):
|
||
while True:
|
||
job = self.book_q.get()
|
||
try:
|
||
self.attempt(job)
|
||
except Exception as e:
|
||
log(f"⚠ attempt error: {e}")
|
||
|
||
def attempt(self, job):
|
||
t_req = time.time()
|
||
try:
|
||
book = get_json(f"{CLOB}/book?token_id={job['asset']}")
|
||
except Exception:
|
||
with self.lock:
|
||
self.state["counters"]["book_fail"] += 1
|
||
self._skip(job, "book_fail", None)
|
||
self.log_attempt({**job, "filled": False, "why": "book_fail"})
|
||
return
|
||
lat_ms = int((time.time() - t_req) * 1000)
|
||
cap = min(job["p_ref"] * (1 + SLIP_CAP), 0.99)
|
||
r = walk_asks(book.get("asks"), cap)
|
||
bid_top = sorted(((float(b["price"]), float(b["size"]))
|
||
for b in book.get("bids") or []),
|
||
reverse=True)[:BID_LEVELS]
|
||
end_ts = None # expected resolution (dashboard ETA)
|
||
if r["filled"]:
|
||
try:
|
||
m = get_json(f"{CLOB}/markets/{job.get('cond')}", timeout=5)
|
||
end_ts = iso_ts(m.get("end_date_iso") or "")
|
||
except Exception:
|
||
pass
|
||
rec = {**job, "cap": round(cap, 4), "latency_ms": lat_ms,
|
||
"top": r["top"], "bid_top": [[p, s] for p, s in bid_top],
|
||
"best_ask": r["best_ask"], "filled": r["filled"]}
|
||
with self.lock:
|
||
c = self.state["counters"]
|
||
if not r["filled"]:
|
||
c["craters"] += 1
|
||
self._skip(job, "crater", r["best_ask"])
|
||
ba = "—" if r["best_ask"] is None else f"{r['best_ask']:.3f}"
|
||
log(f"CRATER {job['title'][:38]} ref {job['p_ref']:.3f} "
|
||
f"ask {ba} flow ${job['flow']} ({lat_ms}ms)")
|
||
else:
|
||
fee = FEE_RATE * r["shares"] * min(r["vwap"], 1 - r["vwap"])
|
||
lot = {**job, "end_ts": end_ts, "price": round(r["vwap"], 5),
|
||
"best_ask": r["best_ask"],
|
||
"shares": round(r["shares"], 4),
|
||
"cost": round(r["usd"], 2), "fee": round(fee, 4),
|
||
"levels": r["levels"], "partial": r["partial"],
|
||
"latency_ms": lat_ms}
|
||
self.state["open"][f"{job['asset']}:{job['ts']}"] = lot
|
||
c["fills"] += 1
|
||
rec.update({"price": lot["price"], "shares": lot["shares"],
|
||
"usd": lot["cost"], "fee": lot["fee"],
|
||
"partial": lot["partial"], "end_ts": end_ts})
|
||
for off in MARKOUT_OFFSETS: # observational exit marks
|
||
if end_ts and job["ts"] + off > end_ts:
|
||
continue
|
||
heapq.heappush(self.mo_heap,
|
||
(job["ts"] + off, job["asset"],
|
||
f"{job['asset']}:{job['ts']}", off))
|
||
log(f"FILL {job['outcome']} · {job['title'][:38]} @ "
|
||
f"{lot['price']:.3f} (${lot['cost']:.2f}"
|
||
f"{' partial' if lot['partial'] else ''}, "
|
||
f"flow ${job['flow']}, {lat_ms}ms)")
|
||
self.persist()
|
||
self.log_attempt(rec)
|
||
|
||
def log_attempt(self, rec):
|
||
"""Append-only full attempt stream — the offline-replay dataset.
|
||
One JSON line per attempt (fills, craters, fails, throttles)."""
|
||
try:
|
||
with open(ATTEMPTS, "a") as fh:
|
||
fh.write(json.dumps(rec) + "\n")
|
||
except Exception as e:
|
||
log(f"⚠ attempts log write failed: {e}")
|
||
|
||
def markout_worker(self):
|
||
"""Observational: due book re-reads -> best bid/ask marks. Never
|
||
touches the signal path; pending marks die on restart (counted)."""
|
||
while True:
|
||
due = None
|
||
with self.lock:
|
||
if self.mo_heap and self.mo_heap[0][0] <= time.time():
|
||
due = heapq.heappop(self.mo_heap)
|
||
if due is None:
|
||
time.sleep(2)
|
||
continue
|
||
due_ts, asset, fid, off = due
|
||
rec = {"fill_id": fid, "asset": asset, "offset_s": off,
|
||
"due_ts": round(due_ts, 1),
|
||
"read_ts": round(time.time(), 3)}
|
||
try:
|
||
book = get_json(f"{CLOB}/book?token_id={asset}")
|
||
bids = sorted(((float(b["price"]), float(b["size"]))
|
||
for b in book.get("bids") or []), reverse=True)
|
||
asks = sorted(((float(a["price"]), float(a["size"]))
|
||
for a in book.get("asks") or []))
|
||
rec["bid"], rec["bid_sz"] = (bids[0] if bids else (None, None))
|
||
rec["ask"], rec["ask_sz"] = (asks[0] if asks else (None, None))
|
||
key = "mo_done"
|
||
except Exception as e:
|
||
rec["err"] = str(e)[:40]
|
||
key = "mo_err"
|
||
with self.lock:
|
||
c = self.state["counters"]
|
||
c[key] = c.get(key, 0) + 1
|
||
try:
|
||
with open(MARKOUTS, "a") as fh:
|
||
fh.write(json.dumps(rec) + "\n")
|
||
except Exception as e:
|
||
log(f"⚠ markouts log write failed: {e}")
|
||
|
||
def _skip(self, job, why, ba): # under lock
|
||
self.state["skips"].append(
|
||
{"ts": int(job["ts"]), "asset": job["asset"], "why": why,
|
||
"p_ref": job["p_ref"], "flow": job["flow"], "ba": ba,
|
||
"title": job["title"]})
|
||
del self.state["skips"][:-SKIPS_TRIM]
|
||
|
||
# ── provisional settles (nightly re-grades with chain truth) ───────────
|
||
def settle_clob_pass(self):
|
||
with self.lock:
|
||
now = time.time()
|
||
due = list(self.state["open"].keys())
|
||
due.sort(key=lambda lid: self.state["open"][lid].get("clob_ck", 0))
|
||
due = due[:CLOB_PASS_CAP]
|
||
snap = {}
|
||
for lid in due:
|
||
lot = self.state["open"][lid]
|
||
lot["clob_ck"] = now # round-robin stamp
|
||
snap[lid] = (lot["cond"], lot["asset"], lot.get("end_ts"))
|
||
verdicts = {}
|
||
ends = {}
|
||
for lid, (cond, asset, end_ts) in snap.items():
|
||
try:
|
||
m = get_json(f"{CLOB}/markets/{cond}")
|
||
except Exception:
|
||
continue
|
||
if end_ts is None: # backfill ETAs for pre-ETA fills
|
||
ends[lid] = iso_ts(m.get("end_date_iso") or "")
|
||
if not m.get("closed"):
|
||
continue
|
||
for t in m.get("tokens") or []:
|
||
if str(t.get("token_id")) == str(asset):
|
||
verdicts[lid] = 1.0 if t.get("winner") else 0.0
|
||
with self.lock:
|
||
for lid, e in ends.items():
|
||
if lid in self.state["open"]:
|
||
self.state["open"][lid]["end_ts"] = e
|
||
n = 0
|
||
for lid, pay in verdicts.items():
|
||
lot = self.state["open"].get(lid)
|
||
if lot:
|
||
self._settle(lid, lot, pay)
|
||
n += 1
|
||
if n or ends:
|
||
self.persist()
|
||
|
||
def _settle(self, lid, lot, pay): # under lock
|
||
pnl = round(lot["shares"] * pay - lot["cost"] - lot["fee"], 2)
|
||
self.state["pnl_realized"] = round(self.state["pnl_realized"] + pnl, 2)
|
||
c = self.state["counters"]
|
||
c["settled_total"] += 1
|
||
c["settle_clob"] += 1
|
||
rec = {**lot, "payout": pay, "provisional": True,
|
||
"settled_ts": int(time.time()), "pnl": pnl}
|
||
self.state["settled"].append(rec)
|
||
del self.state["settled"][:-SETTLED_TRIM]
|
||
del self.state["open"][lid]
|
||
try: # durable append-log: SETTLED_TRIM can
|
||
with open(SETTLES, "a") as fh: # never rotate a settle away
|
||
fh.write(json.dumps(rec) + "\n")
|
||
except Exception as e:
|
||
log(f"⚠ settles log write failed: {e}")
|
||
word = "WON" if pay == 1.0 else "refund" if pay == 0.5 else "lost"
|
||
log(f"SETTLE {word} {lot['title'][:36]} {pnl:+.2f} · "
|
||
f"realized {self.state['pnl_realized']:+.2f}")
|
||
|
||
# ── plumbing ───────────────────────────────────────────────────────────
|
||
def persist(self):
|
||
tmp = STATE + ".tmp"
|
||
json.dump(self.state, open(tmp, "w"))
|
||
os.replace(tmp, STATE)
|
||
|
||
def feed_dict(self): # under lock
|
||
stt = self.state
|
||
return {
|
||
"mode": "paper-measurement", "study": "surge-A (#16)",
|
||
"sem_ver": SEM_VER, "updated": int(time.time()),
|
||
"boot_ts": stt["boot_ts"],
|
||
"frozen": {"flow_usd": FLOW_USD, "window_s": WINDOW_S,
|
||
"band": BAND, "cooldown_s": COOLDOWN_S,
|
||
"stake": STAKE, "fee_rate": FEE_RATE,
|
||
"slip_cap": SLIP_CAP},
|
||
"counters": stt["counters"], "pnl_realized": stt["pnl_realized"],
|
||
"informed": self.set_meta, "open_n": len(stt["open"]),
|
||
"open": [{**l, "id": lid}
|
||
for lid, l in list(stt["open"].items())[-200:]],
|
||
"settled": stt["settled"][-200:], "skips": stt["skips"][-100:],
|
||
"note": "verdict binds to forward_ledger.jsonl (#16); this feed "
|
||
"measures capturability — bankroll specs are replayed "
|
||
"offline from the attempts log (surge_book.json)"}
|
||
|
||
|
||
SUB = json.dumps({"action": "subscribe", "subscriptions": [
|
||
{"topic": "activity", "type": "trades", "filters": ""}]})
|
||
|
||
|
||
def run_conn(tag, bot):
|
||
backoff = 2
|
||
while True:
|
||
state = {"fresh": time.time()}
|
||
|
||
def on_open(ws):
|
||
ws.send(SUB)
|
||
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 /test 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:
|
||
body = json.dumps(bot.feed_dict()).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 selftest():
|
||
"""Boot gate — hard exit on failure."""
|
||
assert niche("LoL: T1 vs GenG map 2") == "esports"
|
||
assert niche("Pittsburgh Pirates vs. New York Yankees: O/U 9.5") == "sports"
|
||
assert niche("Will it rain in NYC") == "other"
|
||
assert event_key("mlb-pit-nyy-2026-07-22-game") == "mlb-pit-nyy-2026-07-22"
|
||
r = walk_asks([{"price": "0.60", "size": "200"},
|
||
{"price": "0.55", "size": "100"}], cap=0.62)
|
||
assert r["filled"] and abs(r["usd"] - 100.0) < 1e-6, r
|
||
assert r["levels"] == 2 and r["best_ask"] == 0.55 and not r["partial"], r
|
||
assert r["top"][0] == [0.55, 100.0] and len(r["top"]) == 2, r
|
||
r = walk_asks([{"price": "0.55", "size": "10"}], cap=0.60)
|
||
assert r["filled"] and r["partial"] and abs(r["usd"] - 5.5) < 1e-9, r
|
||
r = walk_asks([{"price": "0.70", "size": "10"}], cap=0.60)
|
||
assert not r["filled"] and r["best_ask"] == 0.70, r
|
||
assert not walk_asks([], 0.5)["filled"]
|
||
log("selftest OK")
|
||
|
||
|
||
def main():
|
||
selftest()
|
||
bot = Surge()
|
||
bot.load_set()
|
||
threading.Thread(target=serve_feed, args=(bot,), daemon=True).start()
|
||
for _ in range(BOOK_WORKERS):
|
||
threading.Thread(target=bot.book_worker, daemon=True).start()
|
||
for _ in range(2):
|
||
threading.Thread(target=bot.markout_worker, 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
|
||
now = time.time()
|
||
with bot.lock:
|
||
bot.last_trig = {a: t for a, t in bot.last_trig.items()
|
||
if now - t < COOLDOWN_S}
|
||
# trim flow windows for tokens that went quiet
|
||
bot.win = {a: b for a, b in bot.win.items()
|
||
if b and b[-1][0] > now - 2 * WINDOW_S}
|
||
if n % 2 == 0:
|
||
bot.settle_clob_pass()
|
||
if n % 5 == 0:
|
||
with bot.lock:
|
||
bot.persist()
|
||
if time.time() - last_set > 6 * 3600:
|
||
bot.load_set()
|
||
last_set = time.time()
|
||
with bot.lock:
|
||
c = bot.state["counters"]
|
||
msg = (f"paper pnl {bot.state['pnl_realized']:+.2f} · "
|
||
f"open {len(bot.state['open'])} · trig {c['triggers']} "
|
||
f"att {c['attempts']} fill {c['fills']} "
|
||
f"crat {c['craters']} thr {c['throttle_skip']} · "
|
||
f"settled {c['settled_total']}")
|
||
log(msg)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|