mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-28 00:07:47 +00:00
items 2-4: depth gate + guard validated + clob_user own-fill push
DEPTH GATE (copytrade.book_depth + gate in _handle_their_buy, fitted on 131 book-annotated fills): pre-order, skip if spread>0.08 (market mid-move, median |slip| ~14%) or ask5c<$50 (dust books mispriced every fill), else cap the stake at 10% of 5c ask depth (keeps impact <~2%; >20% of depth made >+2% slip 33-50% likely). Fail-open: a failed book fetch declines to bind. This is what lets stakes scale — binds mainly on slow-market wallets like 0xbadaf319 (median 23% of depth at $40-100 stakes). config.depth_gate; paper+live inherit DEFAULT_CONFIG. GUARD: counterfactuals VALIDATED the 0.05 absolute guard — it sits at the EV knee (0.05-0.10 moves ≈ breakeven, >0.10 = -20% ROI). No change, comment records the evidence. (The +9-14% lag-drift tail in deep esports books is a detection-lag problem the depth gate can't see; RTDS ~1s already addresses it — protected max_price is the backstop.) CLOB_USER: UserFillsListener streams our order/trade lifecycle from ws-subscriptions-clob/ws/user (auth = client.credentials, the SDK's derived L2 creds; all-markets, no per-market mgmt). A matching order id triggers an IMMEDIATE resolve_pendings() — in-play holds adopt on match, not on the 60s tick. Events only TRIGGER; get_order stays the arbiter (2026-07-12 anti-phantom-cash invariant intact). resolve_pendings now re-entrancy-locked (ws + heartbeat race). 60s poll stays as the fallback; ws failure degrades to today. Live role only. Also: sub-$1 BUY amount bumped to the $1 venue minimum (share-flooring shaved gated $1.00 stakes to $0.99 — 7 of 13 live rejections). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+136
-1
@@ -348,9 +348,14 @@ class LedgerLiveExecutor:
|
||||
# them). BUY never bounds below the quoted cross; SELL never
|
||||
# bounds above the quoted bid.
|
||||
if side == "BUY":
|
||||
# share-flooring can shave a gated $1.00 stake to $0.99, which
|
||||
# the venue rejects ('invalid amount for a marketable BUY') —
|
||||
# 7 of 13 live rejections in the first 3 days. Bump to the $1
|
||||
# minimum; worst case pays a cent over the gated stake.
|
||||
amt = max(round(sz * price, 2), 1.0)
|
||||
r = self.client.place_market_order(
|
||||
token_id=token_id, side="BUY",
|
||||
amount=round(sz * price, 2),
|
||||
amount=amt,
|
||||
max_price=min(max(round(price * (1 + self._slip), 4),
|
||||
price), 0.99),
|
||||
order_type=self._otype)
|
||||
@@ -609,6 +614,117 @@ class RtdsListener:
|
||||
log(f"rtds handler error: {e}")
|
||||
|
||||
|
||||
# ── own-fill push: the CLOB user channel ─────────────────────────────────────
|
||||
|
||||
class UserFillsListener:
|
||||
"""LIVE only. Streams OUR order/trade lifecycle from the canonical CLOB
|
||||
user channel and turns a matching event into an IMMEDIATE
|
||||
resolve_pendings() pass — in-play holds adopt the second they match
|
||||
instead of on the next 60s tick. Events only TRIGGER the resolver;
|
||||
get_order stays the single arbiter (the 2026-07-12 anti-phantom-cash
|
||||
invariant books nothing from ws payloads). Auth = the CLOB L2 creds the
|
||||
SDK already derived (client.credentials). On any failure the listener
|
||||
logs and stays down while the 60s poll carries — never below today."""
|
||||
|
||||
URL = "wss://ws-subscriptions-clob.polymarket.com/ws/user"
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.state = "off"
|
||||
self.last_msg = 0.0
|
||||
|
||||
def start(self):
|
||||
try:
|
||||
import websocket # noqa: F401
|
||||
creds = self.bot.engine.ex.client.credentials
|
||||
self._auth = {"apiKey": creds.key, "secret": creds.secret,
|
||||
"passphrase": creds.passphrase}
|
||||
except Exception as e:
|
||||
log(f"user-ws: unavailable ({type(e).__name__}: {str(e)[:60]}) — "
|
||||
"pending adoption stays on the 60s poll")
|
||||
return False
|
||||
import websocket
|
||||
threading.Thread(target=self._run, args=(websocket,),
|
||||
daemon=True, name="user-ws").start()
|
||||
return True
|
||||
|
||||
def _pending_ids(self):
|
||||
return {p.get("order_id")
|
||||
for p in self.bot.engine.state.get("pending_orders", [])}
|
||||
|
||||
def _run(self, websocket):
|
||||
backoff = 1
|
||||
|
||||
def on_open(ws):
|
||||
# all-markets subscription: omit "markets" entirely (the SDK's
|
||||
# own UserSpec does the same) — no per-market management needed
|
||||
ws.send(json.dumps({"type": "user", "auth": self._auth}))
|
||||
self.state = "up"
|
||||
self.last_msg = time.time()
|
||||
log("user-ws: connected — own-fill events live")
|
||||
|
||||
def ping():
|
||||
while ws.keep_running:
|
||||
time.sleep(10)
|
||||
try:
|
||||
ws.send("PING")
|
||||
except Exception:
|
||||
break
|
||||
if time.time() - self.last_msg > 30:
|
||||
log("user-ws: silent 30s — forcing reconnect")
|
||||
try:
|
||||
ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
threading.Thread(target=ping, daemon=True).start()
|
||||
|
||||
def on_message(ws, raw):
|
||||
self.last_msg = time.time()
|
||||
if raw == "PONG":
|
||||
return
|
||||
try:
|
||||
m = json.loads(raw)
|
||||
except Exception:
|
||||
return
|
||||
events = m if isinstance(m, list) else [m]
|
||||
hit = False
|
||||
for ev in events:
|
||||
if not isinstance(ev, dict):
|
||||
continue
|
||||
et = ev.get("event_type")
|
||||
ids = set()
|
||||
if et == "order" and ev.get("type") == "UPDATE" \
|
||||
and float(ev.get("size_matched") or 0) > 0:
|
||||
ids.add(ev.get("id"))
|
||||
elif et == "trade" and ev.get("status") in (
|
||||
"MATCHED", "MINED", "CONFIRMED"):
|
||||
ids.add(ev.get("taker_order_id"))
|
||||
for mo in ev.get("maker_orders") or []:
|
||||
if isinstance(mo, dict):
|
||||
ids.add(mo.get("order_id"))
|
||||
if ids & self._pending_ids():
|
||||
hit = True
|
||||
if hit:
|
||||
log("user-ws: pending order matched — resolving now")
|
||||
try:
|
||||
self.bot.resolve_pendings()
|
||||
except Exception as e:
|
||||
log(f"user-ws resolver error: {e}")
|
||||
|
||||
while True:
|
||||
try:
|
||||
app = websocket.WebSocketApp(self.URL, on_open=on_open,
|
||||
on_message=on_message)
|
||||
app.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE})
|
||||
except Exception as e:
|
||||
log(f"user-ws: listener error {str(e)[:80]}")
|
||||
self.state = "down"
|
||||
log(f"user-ws: down — reconnect in {backoff}s (60s poll carries)")
|
||||
time.sleep(backoff)
|
||||
backoff = min(backoff * 2, 120)
|
||||
|
||||
|
||||
# ── the push → filter → execute bridge ──────────────────────────────────────
|
||||
|
||||
class Copybot:
|
||||
@@ -1197,6 +1313,20 @@ class Copybot:
|
||||
pend = st.get("pending_orders") or []
|
||||
if not pend or not self.engine.ex.live:
|
||||
return
|
||||
# user-ws events and the heartbeat both call this — one pass at a
|
||||
# time; a concurrent caller just skips (the running pass adopts)
|
||||
rl = getattr(self, "_resolve_lock", None)
|
||||
if rl is None:
|
||||
rl = self._resolve_lock = threading.Lock()
|
||||
if not rl.acquire(blocking=False):
|
||||
return
|
||||
try:
|
||||
self._resolve_pendings_locked(st)
|
||||
finally:
|
||||
rl.release()
|
||||
|
||||
def _resolve_pendings_locked(self, st):
|
||||
pend = st.get("pending_orders") or []
|
||||
ex = self.engine.ex
|
||||
now = time.time()
|
||||
keep = []
|
||||
@@ -2115,6 +2245,11 @@ def main():
|
||||
bot.rtds.start()
|
||||
else:
|
||||
log("rtds: T0 listener disabled for this role (RTDS_DETECT=1 enables)")
|
||||
if want_live:
|
||||
# own-fill push (2026-07-13): in-play holds adopt the moment they
|
||||
# match instead of on the next 60s resolver tick
|
||||
bot.userws = UserFillsListener(bot)
|
||||
bot.userws.start()
|
||||
|
||||
# on-chain resolution RPC (payout vectors for operator-resolved markets):
|
||||
# env ALCHEMY_RPC_URL wins (the Fly worker has no config.json), else the
|
||||
|
||||
+68
-1
@@ -63,7 +63,15 @@ DEFAULT_CONFIG = {
|
||||
"price_guard_abs": 0.05, # skip if price moved >5 POINTS above their
|
||||
# fill (absolute, 2026-07-10: 0.14→0.15 must
|
||||
# follow; relative % blocked 1-tick moves on
|
||||
# cheap in-play books)
|
||||
# cheap in-play books). VALIDATED 2026-07-13
|
||||
# by the missed-ledger counterfactuals: the
|
||||
# 0.05 line sits at the EV knee (0.05-0.10
|
||||
# moves ≈ breakeven, >0.10 = −20% ROI).
|
||||
"depth_gate": { # 2026-07-13, fitted on 131 gated fills —
|
||||
"max_spread": 0.08, # wider = market mid-move (med |slip| ~14%)
|
||||
"min_ask5c": 50.0, # dust books mispriced every observed fill
|
||||
"max_frac_of_ask5c": 0.10, # stake ≤10% of 5c ask depth (impact <~2%)
|
||||
},
|
||||
"risk": {
|
||||
"max_trade_usd": 50.0, # hard ceiling on any single copy
|
||||
"max_position_usd": 40.0, # hard ceiling on total cost in one market
|
||||
@@ -145,6 +153,32 @@ def clob_price(token_id, side):
|
||||
return None
|
||||
|
||||
|
||||
def book_depth(token_id):
|
||||
"""Top-of-book + $-depth within 5c of touch — the DEPTH GATE's input
|
||||
(2026-07-13, fitted on 131 gated fills). Returns {bb, ba, spread, bid5c,
|
||||
ask5c} or None on any failure (the gate then declines to bind — the
|
||||
price guard and protected prices still bound the copy)."""
|
||||
try:
|
||||
url = f"{CLOB_API}/book?token_id={token_id}"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=8, context=SSL_CTX) as r:
|
||||
b = json.loads(r.read().decode())
|
||||
bids, asks = b.get("bids") or [], b.get("asks") or []
|
||||
bb = max((float(x["price"]) for x in bids), default=None)
|
||||
ba = min((float(x["price"]) for x in asks), default=None)
|
||||
|
||||
def depth(side, ref, sgn):
|
||||
if ref is None:
|
||||
return None
|
||||
return round(sum(float(x["size"]) * float(x["price"]) for x in side
|
||||
if sgn * (float(x["price"]) - ref) >= -0.05), 2)
|
||||
return {"bb": bb, "ba": ba,
|
||||
"spread": round(ba - bb, 4) if bb is not None and ba is not None else None,
|
||||
"bid5c": depth(bids, bb, 1), "ask5c": depth(asks, ba, -1)}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def their_positions(wallet):
|
||||
"""Current open positions -> {token_id: shares}, for exit-fraction math.
|
||||
Cap is generous: a whale can hold >500 open positions, and a position
|
||||
@@ -499,6 +533,39 @@ class CopyTrader:
|
||||
self.record_miss(wallet, token, cond, title, outcome, price,
|
||||
want_usd, reason)
|
||||
return
|
||||
# DEPTH GATE (2026-07-13, fitted on 131 book-annotated fills): the
|
||||
# book must absorb the stake. Fills into <$90 of 5c-depth paid
|
||||
# +2.6–4.2%; >20% of visible depth made >+2% slippage 33–50% likely;
|
||||
# spread>0.08 books ran median |slip| ~14% (market mid-move). Shrink
|
||||
# to 10% of depth, skip dust/unreliable books. A failed book fetch
|
||||
# declines to bind — guard + protected prices still bound the copy.
|
||||
dg = self.cfg.get("depth_gate")
|
||||
if dg:
|
||||
bk = book_depth(token)
|
||||
if bk and bk.get("ask5c") is not None:
|
||||
d_reason = None
|
||||
if (bk.get("spread") or 0) > dg["max_spread"]:
|
||||
d_reason = (f"book unreliable (spread {bk['spread']:.2f} > "
|
||||
f"{dg['max_spread']:.2f})")
|
||||
elif bk["ask5c"] < dg["min_ask5c"]:
|
||||
d_reason = (f"thin book (${bk['ask5c']:.0f} within 5c < "
|
||||
f"${dg['min_ask5c']:.0f})")
|
||||
else:
|
||||
cap = dg["max_frac_of_ask5c"] * bk["ask5c"]
|
||||
if cap < self.risk["min_order_usd"]:
|
||||
d_reason = (f"depth cap ${cap:.2f} below min order "
|
||||
f"(ask5c ${bk['ask5c']:.0f})")
|
||||
elif cap < allowed:
|
||||
self.log(f"{kind} {label} — depth gate shrinks "
|
||||
f"${allowed:.2f} → ${cap:.2f} "
|
||||
f"(10% of ${bk['ask5c']:.0f} ask depth)")
|
||||
allowed = cap
|
||||
if d_reason:
|
||||
self.log(f"{kind} {label} — skip ({d_reason})")
|
||||
if not is_add:
|
||||
self.record_miss(wallet, token, cond, title, outcome,
|
||||
price, allowed, d_reason)
|
||||
return
|
||||
# ONE outstanding in-play hold per token: a second order while a
|
||||
# pending rests re-buys the same signal and poisons the resolver's
|
||||
# balance-diff window (2026-07-12: overlapping pendings booked one
|
||||
|
||||
Reference in New Issue
Block a user