live executor ported to the unified SDK (polymarket-client)

py-clob-client is archived; the CLOB rejects its orders globally. New
LedgerLiveExecutor: SecureClient.create(private_key) — deposit wallet
auto-resolves, no api_key at runtime; place_market_order FAK/FOK with
protected prices (quoted ±live.max_slippage_pct, default 5%, clamped to
[0.01,0.99]); fills parsed from AcceptedOrder (BUY: making=USD given,
taking=shares got; SELL reversed — semantics PROVEN by today's $5 round
trip: buy matched 7.35294 @ 0.68, sell matched 7.35 @ 0.67). Still never
raises into the trade loop. chain_cash_gap repointed at the deposit
wallet's pUSD (was the emptied legacy proxy → CASH≠CHAIN +24.73 alarm).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-10 01:08:18 -04:00
parent 0cf163abe5
commit c5b87afcab
+66 -54
View File
@@ -239,76 +239,86 @@ class LedgerPaperExecutor(PaperExecutor):
return r return r
class LedgerLiveExecutor(LiveExecutor): class LedgerLiveExecutor:
"""Live executor with two production fixes over the base GTC executor: """Live executor on the unified SDK (polymarket-client). py-clob-client was
ARCHIVED May 2026 — the CLOB rejects its order format globally ('invalid
order version'), which is why this no longer extends LiveExecutor.
* **Marketable Fill-Or-Kill orders** (gap 3) — a copy either fills * **Marketable FAK/FOK** via place_market_order — the SDK owns tick
immediately at a crossing price or is cleanly killed, never left resting conformity, neg-risk exchange routing, and fee handling internally
on the book half-filled. Order type is configurable (live.order_type: (the hand-rolled tick rounding here crashed the bot twice on
FOK all-or-nothing, or FAK fill-what-you-can-then-kill). 2026-07-09; all of that machinery is gone).
* **Fill recording** — same ledger as paper, so cash/lag/slippage tracking * **Protected prices** — max_price (BUY) / min_price (SELL) bound the
works live too. filled_shares comes from the match response when present. fill at the engine's quoted price ± live.max_slippage_pct (default
5%), replacing the old round-to-tick limit. Clamped to [0.01, 0.99],
valid on both 1c and 0.1c books, so the SDK's band check can't raise.
* **Fill recording** — same ledger as paper. AcceptedOrder reports the
matched amounts: BUY gives collateral (making) for shares (taking),
SELL the reverse; avg fill price falls out of the ratio.
* **Never raises into the trade loop** — any exception is an honest
ok:False (the engine records a missed row).
""" """
live = True
def __init__(self, cfg): def __init__(self, cfg):
super().__init__(cfg) try:
from polymarket import SecureClient
except ImportError:
sys.exit("Live mode needs the unified SDK: "
"pip install --pre polymarket-client")
live = cfg.get("live", {})
if not live.get("private_key"):
sys.exit("Live mode needs live.private_key in the config.")
# wallet auto-resolves to the signer's Deposit Wallet; no api_key at
# runtime — trading approvals already exist (host/order_probe_v2.py).
# create() is ready to use as-is (__enter__ is a no-op returning self).
self.client = SecureClient.create(private_key=live["private_key"])
self.fills = [] self.fills = []
name = cfg.get("live", {}).get("order_type", "FOK").upper() ot = str(live.get("order_type", "FAK")).upper()
self._otype = getattr(self._OrderType, name, self._OrderType.FOK) self._otype = ot if ot in ("FAK", "FOK") else "FAK"
self._slip = float(live.get("max_slippage_pct", 0.05))
def _tick(self, token_id):
"""Market tick size, cached — a 3dp price on a 1c-tick book raises
PolyApiException, which (uncaught) crashed the bot at 21:21 and 21:53
on 2026-07-09: the first two qualifying live signals. Fallback 0.01 is
valid on every book (a 1c multiple is also a 0.1c multiple)."""
cache = getattr(self, "_ticks", None)
if cache is None:
cache = self._ticks = {}
if token_id not in cache:
try:
cache[token_id] = float(self.client.get_tick_size(token_id))
except Exception:
cache[token_id] = 0.01
return cache[token_id]
def _order(self, token_id, shares, price, side): def _order(self, token_id, shares, price, side):
import math import math
tick = self._tick(token_id) sz = math.floor(shares * 100) / 100.0 # cost never exceeds the gated stake
# buys round UP to the tick (stay marketable/crossing), sells DOWN;
# size floors to 2dp so cost never exceeds the gated stake
steps = price / tick
px = round((math.ceil(steps) if side == self._BUY else math.floor(steps)) * tick, 4)
px = min(max(px, tick), 1 - tick)
sz = math.floor(shares * 100) / 100.0
try: try:
args = self._OrderArgs(price=px, size=sz, side=side, token_id=token_id) if side == "BUY":
signed = self.client.create_order(args) r = self.client.place_market_order(
resp = self.client.post_order(signed, self._otype) # marketable FOK/FAK token_id=token_id, side="BUY",
amount=round(sz * price, 2),
max_price=min(round(price * (1 + self._slip), 2), 0.99),
order_type=self._otype)
else:
r = self.client.place_market_order(
token_id=token_id, side="SELL", shares=sz,
min_price=max(round(price * (1 - self._slip), 2), 0.01),
order_type=self._otype)
except Exception as e: # NEVER raise into the trade loop except Exception as e: # NEVER raise into the trade loop
return {"ok": False, "filled_shares": 0.0, "price": px, return {"ok": False, "filled_shares": 0.0, "price": price,
"resp": f"exception: {e}", "paper": False} "resp": f"exception: {e}", "paper": False}
price = px if not getattr(r, "ok", False): # RejectedOrder: typed code + message
shares = sz return {"ok": False, "filled_shares": 0.0, "price": price,
ok = bool(resp and resp.get("success", True)) "resp": f"{getattr(r, 'code', '?')}: {getattr(r, 'message', r)}",
filled = shares if ok else 0.0 "paper": False}
for k in ("sizeMatched", "size_matched", "makingAmount"): # use real fill if reported making = float(r.making_amount or 0) # what we gave (matched)
if resp and resp.get(k): taking = float(r.taking_amount or 0) # what we got (matched)
try: filled, usd = (taking, making) if side == "BUY" else (making, taking)
filled = float(resp[k]); break return {"ok": filled > 0, "filled_shares": filled,
except (TypeError, ValueError): "price": usd / filled if filled else price,
pass "resp": {"order_id": r.order_id, "status": r.status,
return {"ok": ok and filled > 0, "filled_shares": filled, "price": price, "making": making, "taking": taking,
"resp": resp, "paper": False} "trades": len(r.trade_ids)},
"paper": False}
def buy(self, token_id, shares, price, meta): def buy(self, token_id, shares, price, meta):
r = self._order(token_id, shares, price, self._BUY) r = self._order(token_id, shares, price, "BUY")
if r["ok"]: if r["ok"]:
self.fills.append({"side": "BUY", "token": token_id, self.fills.append({"side": "BUY", "token": token_id,
"shares": r["filled_shares"], "price": r["price"]}) "shares": r["filled_shares"], "price": r["price"]})
return r return r
def sell(self, token_id, shares, price, meta): def sell(self, token_id, shares, price, meta):
r = self._order(token_id, shares, price, self._SELL) r = self._order(token_id, shares, price, "SELL")
if r["ok"]: if r["ok"]:
self.fills.append({"side": "SELL", "token": token_id, self.fills.append({"side": "SELL", "token": token_id,
"shares": r["filled_shares"], "price": r["price"]}) "shares": r["filled_shares"], "price": r["price"]})
@@ -960,10 +970,12 @@ class Copybot:
ts, bal = self._chain_bal ts, bal = self._chain_bal
if now - ts > 60: if now - ts > 60:
try: try:
from py_clob_client.clob_types import BalanceAllowanceParams, AssetType # unified SDK: the deposit wallet's pUSD as the exchange counts
# it (LIVE_ROLLOUT 1.4 anchor — was the emptied legacy proxy
# via py-clob-client, which alarmed CASH≠CHAIN +24.73)
r = self.engine.ex.client.get_balance_allowance( r = self.engine.ex.client.get_balance_allowance(
BalanceAllowanceParams(asset_type=AssetType.COLLATERAL)) asset_type="COLLATERAL")
bal = int(r.get("balance", 0)) / 1e6 bal = r.balance / 1e6
self._chain_bal = (now, bal) self._chain_bal = (now, bal)
except Exception: except Exception:
return None return None