surgebot: Study A paper harness, deploy-ready for the Friday sprint (#16)

Real-time PAPER trader of the FROZEN surge signal on its own Fly app
(wwf-surgebot, recorder-pattern image, no keys, no bot imports):
$100 paper book · 5%-of-equity daily stakes ($1 venue floor) · cash-gated
all-or-nothing · event cap 2 · paper FAK against the live CLOB book inside
p_ref*1.05 · provisional CLOB settles re-graded nightly with CTF payout
vectors (grade_surge.py -> surge_paper_ledger.jsonl). Informed set
published daily (informed_set.py -> params/informed_set.json, frozen
method). Unit-tested: fill/crater/event-cap/cash paths exact; live smoke:
dual sockets + sizing clean. Believing any of it stays gated on the #16
forward verdict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-21 15:15:52 -04:00
parent e8eb0a7dbd
commit eeeed39e22
7 changed files with 622 additions and 1 deletions
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Nightly chain-truth grading of the surge paper harness (wwf-surgebot).
Pulls /data/surge_state.json off the box (read-only — never writes back),
re-grades every settled entry with CTF payout vectors (payouts.truth; the
harness's provisional CLOB winner flags lie on operator-resolved markets,
[[polymarket-resolution-truth]]), and appends one row per settle to
research/surge_paper_ledger.jsonl (keyed asset+ts, idempotent). Also emits
the running paper-book summary the #16 sprint decision reads."""
import json
import os
import shutil
import subprocess
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
LEDGER = os.path.join(HERE, "surge_paper_ledger.jsonl")
TMP = os.path.join(HERE, ".surge_state.pull.json")
FLYCTL = shutil.which("flyctl") or "/opt/homebrew/bin/flyctl"
def main():
r = subprocess.run([FLYCTL, "ssh", "sftp", "get", "/data/surge_state.json",
TMP, "-a", "wwf-surgebot"], capture_output=True,
timeout=300, stdin=subprocess.DEVNULL)
if not os.path.exists(TMP) or os.path.getsize(TMP) == 0:
print("[grade_surge] box unreachable or no state yet — skip")
return 0
st = json.load(open(TMP))
os.remove(TMP)
sys.path.insert(0, os.path.join(HERE, "..", "live"))
import payouts
have = set()
try:
for ln in open(LEDGER):
try:
d = json.loads(ln)
have.add((d["asset"], d["ts"]))
except Exception:
pass
except FileNotFoundError:
pass
settled = st.get("settled", [])
new = [s for s in settled if (s["asset"], s["ts"]) not in have]
if new:
payouts.ensure(sorted({s["cond"] for s in new if s.get("cond")}))
graded = flips = 0
with open(LEDGER, "a") as fh:
for s in new:
t = payouts.truth(s.get("cond"), s.get("asset")) if s.get("cond") else None
pay = s["payout"] if t is None else t
pnl = round(s["shares"] * pay - s["cost"] - s["fee"], 2)
if t is not None and abs(t - s["payout"]) > 1e-9:
flips += 1
fh.write(json.dumps({**s, "chain_payout": pay, "chain_pnl": pnl,
"flip": t is not None
and abs(t - s["payout"]) > 1e-9}) + "\n")
graded += 1
total = wins = 0
pnl_sum = 0.0
try:
for ln in open(LEDGER):
d = json.loads(ln)
total += 1
wins += d["chain_payout"] == 1.0
pnl_sum += d["chain_pnl"]
except FileNotFoundError:
pass
c = st.get("counters", {})
print(f"[grade_surge] +{graded} settles ({flips} provisional flips) · "
f"book: {total} settled {wins}W · chain P&L ${pnl_sum:+.2f} · "
f"cash ${st.get('cash', 0):.2f} · open {len(st.get('open', {}))} · "
f"lifetime trig {c.get('triggers')} fill {c.get('fills')} "
f"crater {c.get('craters')}")
return 0
if __name__ == "__main__":
sys.exit(main())
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""Publish the frozen-method informed set for the surge paper harness.
Writes params/informed_set.json — the top-150 wallets by the FROZEN Study-A
scoring (study_flow.informed_set, params/study_flow.json), as-of now. The
wwf-surgebot worker fetches this from raw.githubusercontent at boot and
every 6h; nightly.sh regenerates + commits it daily so the harness trades
the same signal the forward ledger scores. Identity showed NO lift over
random controls (#16) — the set is kept anyway because the FROZEN signal
definition uses it, and the verdict must validate exactly what was frozen.
"""
import json
import os
import time
import tape
import study_flow as sf
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "params", "informed_set.json")
def main():
db = tape.connect()
now = db.execute("SELECT max(ts) FROM trades").fetchone()[0]
P = json.load(open(os.path.join(HERE, "params", "study_flow.json")))
fz = P["frozen"]
wallets = sf.informed_set(db, now, fz["top_n"])
json.dump({"generated_at": int(time.time()), "as_of_tape_ts": int(now),
"method": "study_flow.informed_set FROZEN "
f"(top_n={fz['top_n']}, z>={sf.SET_MIN_Z}, "
f"n>={sf.SET_MIN_BETS})",
"wallets": wallets},
open(OUT, "w"), indent=1)
print(f"[informed_set] {len(wallets)} wallets -> {OUT}")
if __name__ == "__main__":
main()
+4 -1
View File
@@ -51,9 +51,12 @@ while true; do
done
"$PY" forward.py >> forward.log 2>&1
"$PY" informed_set.py >> forward.log 2>&1 # surge harness reads this daily
"$PY" grade_surge.py >> forward.log 2>&1 || true # paper book -> chain truth
cd ..
git add research/forward_ledger.jsonl
git add research/forward_ledger.jsonl research/params/informed_set.json \
research/surge_paper_ledger.jsonl 2>/dev/null
if ! git diff --cached --quiet; then
git commit -q -m "research: forward ledger $(date -u +%F) [skip ci]"
git pull --rebase --autostash -q && git push -q
+157
View File
@@ -0,0 +1,157 @@
{
"generated_at": 1784661151,
"as_of_tape_ts": 1784660384,
"method": "study_flow.informed_set FROZEN (top_n=150, z>=2.5, n>=6)",
"wallets": [
"0xda770326bd091d26f816917aa83a6baff668683f",
"0x893575c7d99542163c6b6e8a0fe5af0b6d217daa",
"0xa9f5e5c9c761a0c7d692ba479028f778a5dd937f",
"0xc7d02944a76b9f83b199e9090ecc92c82d241f8a",
"0xc4fe817476491890086330a639ac417d51bf601f",
"0x204f72f35326db932158cba6adff0b9a1da95e14",
"0x945a49252f772a10c6ddd1d1e1e24ee20438a48c",
"0xc29198ad764bd6adaf7bb971a3757a689ece5d74",
"0xfe787d2da716d60e8acff57fb87eb13cd4d10319",
"0xdbdd45150249e229eb4ca8aa48a30dca21faa5de",
"0x2005d16a84ceefa912d4e380cd32e7ff827875ea",
"0xdb83e85ffd22faa4009273034770f96ffc5b1e50",
"0x200ffb575b936d297fcf39076ddf2b476ae07a96",
"0x076daa87c4fe1a85402a9b6b8e0a866224388d4c",
"0xe91171f655be1568e4c63f29663c0028649e3d4e",
"0x2f26606ccc45a934838a97d44a30c32012ae0d8f",
"0xd9c9651bfd445b2cddf750662ee79a755f216686",
"0x03bf6d028f9bbc44a8b90d4b195ecbfe54c324b9",
"0xf1f2b3c6c5bea42a6664fe3a8395addc1ab28c8d",
"0x34045be5c902268d74219ebfe7caa347cfae10d2",
"0x904ed7c7820a434ea9b11e7333ed69190d850bec",
"0x0d2d845a6ff64e31e04a70afce8a573940767ff5",
"0x67af214764bd6eda61fadb25cdcbea6d13d761ee",
"0xbe0c69b37d57a0e2f171b606adab285e56fec0cd",
"0x0ec7f0eac6c47934ab9b41e02bd8f15a5f4293d1",
"0xa509ae942ac8d8bc3f8c5df30cb6ae2c9b13ae46",
"0x592997c2d8119d82d71f21119957c6434d244684",
"0xa951006f1ce68498c1aef9b013880459e6e08a2f",
"0x489d3df3601288940f89d56b5642b0951129f6e3",
"0xa6db8383b756f1e58c504ff4179e19427dd73155",
"0x2e3c40fa47b27c676ddd573064162f57d51508ba",
"0x47138dc1eef25f1ea91f3b2fda0e0f455c634d21",
"0x48363a6cd08b9602e9f2e0523c4f9a89d4c4859b",
"0x505f23d257d23e4a1ec37ff9bca7c88f3bf0645e",
"0xebff466d6070548b13a432d7583e9fa5bce870e5",
"0xa3c2ec158f222f3f4f5f4f57b113d761a4d3df3d",
"0x0c99d9f172fc4c25fd2aa92f9fc272da5f020c88",
"0xd13c1e677498544774850a8d34b210887ec798a7",
"0x2da325618fff9b2ffe64a49253fbcae8c7b9f258",
"0x84ad9c5c547a82ec9a08547b94bd922446e5bfb7",
"0x8d0c8253703b8febbe53f45d182671c5385dc90a",
"0x65228958f3c7e09f09ce2dab06813d6051333a5a",
"0xadfb6cba33cebca02eab6111ace1e3924b9cc2ef",
"0xae4160ad50090eb38c6792e4e00caa512ff88311",
"0x98db8cca55c32b24cfb414b5b43d273f4e1fdd17",
"0x2a69660046d7acc4ab204d7cc5ba78b0776cd2f7",
"0x521070c99db06e54af5e8e4a91d6858decdfbd53",
"0x60ec17443af511a21945f01430e61c803465f7b0",
"0xf504d6bcf88d485d21f65398b9deb04d92180111",
"0x67011de1721bd8af42522c8eacbb4c52d518d7d2",
"0x5d634050ad89f172afb340437ed3170eaa2c9075",
"0x6d3c5bd13984b2de47c3a88ddc455309aab3d294",
"0x0cf18b4294b05509c44a6a0172eff7e97b20d7fe",
"0x77de6e0a24dc57a4345c987eb061d46eb6e5e67a",
"0x83182e6abe7101f2084eef8d570ebdfc66dc34a6",
"0x14e72e19ea2f6e2be41504dd3268184ebe5fa32c",
"0x47c9014f76660cd2efe17c1f5ea342fd12cd2038",
"0x9ab303a355bf22a29e485d98fb88d140abb43044",
"0x4a5147d7af926edc3054c2e4cfe9becd36c14ef6",
"0x237f21a15396d2e56c7ca5f74cfbc6af9dade76e",
"0x9d94f602535e518ee1cb6aade0ca9569f1b1017d",
"0x717990979ff84a32d47205a7eede94317aab7d79",
"0xc75b0390b025086df22363d6c22602506c3b3ea7",
"0x52d93dcf81444f793dda7fedc370f7a35101fb1a",
"0x162147bf9d4588675de814fce9ce36d3b2c0f040",
"0x273d0a3b3e0652465ee8baf8b30c1acad8befb63",
"0xa032d2358fa08860ba30ba8e8d828b7738202090",
"0x7d0caf83488431bb760f204d52bdf5246e2fa3b7",
"0x2126c12a2eb40b10acd1d1d0bda4cea42d58aeeb",
"0xbb8ef6381e4a35b4124584cfce1c73137e2508c1",
"0xeea3f08e8a36081233e4c4b4142fd61a7b56a6a2",
"0x6b23c73159b753c96d3c1ed94413f113300c04a3",
"0x37c1ff27d21b08d1cae9f38453b895eae2a78de1",
"0xfb0c8b014cc976a606e924b5fc4a93b31fd98c72",
"0x0346afae2603313d2bbee96b628536c8cbe352a5",
"0xb6d2d81518cda8f9025697f942b2533ad4efd67b",
"0x9e20c1016cfdf518c84aa398d64bf9b2277a5cf9",
"0x747fda2528435ab2719fdc36d07b4248af61f2d7",
"0x1cae27a4deb610229d264199b655f2b1a4d5d01c",
"0x5268527977f700f9bf9b6d5cd843859e4e70135d",
"0xf1ee4842fa0c7283632185e42577027452d4519c",
"0xd5a32eba30f8baa08c3f12880ff6525f926d5b70",
"0xe83abacce6bf35b11b8de4c890ea4bf19d218355",
"0x32b484581fc5606de9c1e43af4636b6be9bc8b21",
"0x7a5d94f83ff0195387eaf9f4463433d03eda54db",
"0x14f3cee5d890a61fe427407f340d190eab7a0796",
"0xbfd770b7ff07fb2ece41b0e7c593db65a84cf0bf",
"0x0df758f3a438f88d9b98da42ab76cd585a101ec1",
"0x7d0e41915dd94259bbe9dae12d33b1c26d304f01",
"0x1ba2b1137313b248b03a9be40093b23d7e96d212",
"0xc846f5368ab49a854d2280b7ecff92669824c413",
"0x7d9a514f9da9e8aa7fa37306943c7d1720d805e6",
"0x94d22bf045f9a1d1cb37a40a4a8c86fd8917cfbd",
"0x224f7ef690d952cf551a471846d5afb4892e514a",
"0xff34c2cc46e7624310f3549ea355540d57a4694b",
"0x7547479d43f4b52d892a6f2254050eea50edd158",
"0x80d8dddcfdc075eb70f6d2d774a27bfb255ab703",
"0x5a218c7ad04135830a45c41aaed7294df7809318",
"0x4f29e103339919c4baaea2a60195cf1c8bb27a7e",
"0x1df231bd33dae91463e1682927e8096b82c289a0",
"0xc1fd8001916292efc9343e2d555bb67e9edb10cc",
"0x77bcf759a5bbbf604a52d839d5f68ead4c78b91b",
"0x2e0bf01211c2bc155b930ffeb06b67b97dd11435",
"0x664a25f165a0150ce687410470f3352f5783fc5f",
"0x5b8990db8c1304d56cf8466e443b5e50a47f23fb",
"0x4462e46cf0d31466693058893f4913e2411fbfdf",
"0x1a9eb0213ca31d612381841bd6159071aab1a24b",
"0x2c475c09b18fa388f3c60cacef6e7221b00b47f5",
"0xbac4ab50a9059b89131cf7bad38bb12c031586db",
"0x64d4c540211c48ce4cb7b853ffcb0df2b438f056",
"0x26c283df72be6f746b6acd37b7b06ff89da60cdf",
"0x56cc7a163d7a56134a394b76abaa47c7bdcad589",
"0x1a1e0ac0b3ecc0b5bb133af94a26d5236557f992",
"0xb91cdd2b03d05f67db8ed560fb56cca7f668ed66",
"0x083d6a36c1b5e0178aa18d6d19814c73cf7d10a1",
"0xde9f7f4e77a1595623ceb58e469f776257ccd43c",
"0x83d9a38fbf2265d1a092499a964412dd44e810d9",
"0xc2bec11a79567ea92f7c5dfa6587eadaede6af12",
"0xeda9da7fe9a6992128f5008ddbd7156cef74f624",
"0x4126330ba61224a3b265921fcdea1adc50d6e68b",
"0xa3b48b1e1e3c44c2c8181092206f65f29bce88df",
"0x99f0d31fdced5b3a0e5ee2867730a6644a6c9495",
"0x466658e9a04e4586fb0ea46abf0a2fd8902f3a55",
"0x2c46ffe86ee500875db3c679e52f6e64854cdac9",
"0x2d6ac4f70307102ac46e9e6ded67f3838ddf8add",
"0x3e43bfa5be2efe93a621bc17edc934095b100306",
"0x58a84177426c8be517141558baaac252153ba3c7",
"0x125a733601a947640cd928d4fd1bd5ddb841df71",
"0x99d4291db7710e216a8b8f95323385442cc47c0d",
"0xb8fc63b6b017d941510cd32e275058c40171b543",
"0x325f0e5b5afc66ff54102d850f67f0c621304511",
"0x26d61145a64b1b5036c82a147dfeb7d56fa0cacc",
"0xd37ab1a5eeb25696c568e20e94af72b8d3422358",
"0x7ddeb72e6b8726b146d085c20a90bb64158030b1",
"0x91ac00e15e85a986f4d3d035e3d9b1b4ac435ff3",
"0x999a0341c09fb5512feba1cbfb84c1d278dd9a25",
"0xc62fc154efd70e3a7991726c4eef6118566c1781",
"0x98883c610e4400dd998f81cf41658d51ff06e6fe",
"0x1a59d3f300b446d78a42941ab1b71dd61273a57e",
"0x93078b5dd1c34d0f288d2f51b23f1347520d9da3",
"0x8845c34e8ba60527de59575bdc4b5ab70922360e",
"0xd03450276d4aa4069ce11bac4cfb1683a724eece",
"0xa1a77ea9382bb8c3610f3303b66e093f644aace4",
"0xf4c70c115ce5d717073bea78e02995df6635b8e1",
"0x6485f47d0344c03eb4340f985159f6eb2dcba265",
"0xfe8a28be83ba295a9384f72f240d2cfa96bc23ce",
"0xc3a7bca628ccd2fd4bfc72f86c1f257e96e43172",
"0x2529c68db36ce8aa64c64affe620267744a3548a",
"0x4daaedfa9503e4f6981e1d53b1931dc932639f01",
"0x6469297e9e08ed8fb630a3fa4699fdd3899fd7ca"
]
}
+6
View File
@@ -0,0 +1,6 @@
# wwf-surgebot — one dep, no git, no clone (a paper harness must not depend
# on GitHub being up to boot; informed set fetch degrades gracefully).
FROM python:3.12-slim
RUN pip install --no-cache-dir websocket-client
COPY research/surgebot.py /surgebot.py
CMD ["python3", "-u", "/surgebot.py"]
+317
View File
@@ -0,0 +1,317 @@
#!/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 main():
bot = Surge()
bot.load_set()
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()