FIX: pending-adoption oversell — bounded arbiter + per-token serialization
2026-07-12 phantom-cash incident (+$7.86 book-only, alarms caught it): overlapping pendings on one token each read the same balance drop via max(matched, diff) — one real 1.48-share sell booked 3×. No real money moved wrongly (the CLOB rejects oversells); the book just self-inflated. Fixes: (1) order's own size caps every adoption fill; (2) the balance diff is consulted ONLY when the exchange no longer answers for the order (a definitive killed/0 is final); (3) SELL adoptions cap at book holdings; (4) ONE outstanding pending per token — engine buy/sell and exit-retries refuse while the resolver owns a token. Regression suite: the incident scenario books exactly the real fill; phantoms zero. Surgery (machine stopped): cash → chain $11.206, the two poisoned bets corrected from real chain fills (residual adjustment −$0.08), drift 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -496,6 +496,15 @@ class CopyTrader:
|
||||
self.record_miss(wallet, token, cond, title, outcome, price,
|
||||
want_usd, 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
|
||||
# real fill several times → +$7.86 phantom cash).
|
||||
if any(po.get("token") == token
|
||||
for po in self.state.get("pending_orders", [])):
|
||||
self.log(f"{kind} {label} — skip (in-play hold already pending "
|
||||
"on this token)")
|
||||
return
|
||||
shares = allowed / price
|
||||
res = self.ex.buy(token, shares, price, {"title": title})
|
||||
if not res["ok"]:
|
||||
@@ -553,6 +562,11 @@ class CopyTrader:
|
||||
price = self._live_price(token, "sell")
|
||||
if price is None:
|
||||
return
|
||||
if any(po.get("token") == token
|
||||
for po in self.state.get("pending_orders", [])):
|
||||
self.log(f"EXIT {label} — skip (in-play hold already pending on "
|
||||
"this token; resolver owns it)")
|
||||
return
|
||||
res = self.ex.sell(token, sell_shares, price, {})
|
||||
if not res["ok"]:
|
||||
if res.get("pending"): # in-play hold — resolver adopts
|
||||
|
||||
Reference in New Issue
Block a user