mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
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:
@@ -0,0 +1,20 @@
|
||||
# wwf-surgebot — Study A surge-momentum PAPER harness (research silo).
|
||||
# No keys, no orders, no repo clone: code baked in image (recorder pattern).
|
||||
# Believing its numbers is gated on the #16 forward verdict; see the
|
||||
# sprint-plan issue. State on the volume; the Mac nightly pulls + grades.
|
||||
app = "wwf-surgebot"
|
||||
primary_region = "arn"
|
||||
|
||||
[build]
|
||||
dockerfile = "research/surgebot.Dockerfile"
|
||||
|
||||
[[mounts]]
|
||||
source = "surge"
|
||||
destination = "/data"
|
||||
|
||||
[[restart]]
|
||||
policy = "always"
|
||||
|
||||
[[vm]]
|
||||
size = "shared-cpu-1x"
|
||||
memory = "256mb"
|
||||
@@ -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())
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user