mirror of
https://github.com/jaxperro/winning-wallet-finder.git
synced 2026-07-27 15:57:47 +00:00
INCIDENT FIX: in-play 'delayed' acceptances were logged as misses and filled untracked
2026-07-10 09:10-13:14Z: six $5 in-play esports copies returned AcceptedOrder status 'delayed' with zero matched; the executor read matched==0 as a rejection, recorded honest misses, and forgot the orders — which then filled at the exchange minutes later. Book went blind (6 untracked fills, ~$30 real spend vs $5 booked), caps stopped binding, exits never mirrored. Net damage contained by the CLOB's own balance checks: −$2.7 incl fees, two legs won. Executor invariant now: NO order outlives _order() untracked. Zero-matched acceptances poll get_order → cancel remainder → measure truth via the exchange's CONDITIONAL balance diff; exception paths sweep open orders on the token the same way. 5 stub-client unit paths pass. State surgery (machine disarmed first): cash reset to chain ($10.03), six fills adopted as bets (4 settled −$4.38 w/ fees, Aurora G2 open+won), spend tracker set to the real $35/day, false miss rows purged, one documented -0.59 adjustment (bank-vs-deposit + probe). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+66
-3
@@ -278,9 +278,60 @@ class LedgerLiveExecutor:
|
||||
self._otype = ot if ot in ("FAK", "FOK") else "FAK"
|
||||
self._slip = float(live.get("max_slippage_pct", 0.05))
|
||||
|
||||
def _shares_held(self, token_id):
|
||||
"""Exchange-view share balance for a token — chain truth, the arbiter
|
||||
when an order's fate is ambiguous."""
|
||||
b = self.client.get_balance_allowance(asset_type="CONDITIONAL",
|
||||
token_id=str(token_id))
|
||||
return b.balance / 1e6
|
||||
|
||||
def _settle_uncertain(self, token_id, side, bal0, price, order_id=None,
|
||||
deadline_s=20):
|
||||
"""An order may be resting/held at the exchange (in-play 'delayed'
|
||||
acceptance, or an exception after posting). Poll it to a terminal
|
||||
state, cancel whatever remains, and return (filled, avg_price) from
|
||||
the exchange's own balance diff. INVARIANT (2026-07-10 incident: six
|
||||
in-play acceptances were logged as misses and filled untracked
|
||||
minutes later): no order outlives this call untracked."""
|
||||
import time
|
||||
px = price
|
||||
deadline = time.time() + deadline_s
|
||||
while order_id and time.time() < deadline:
|
||||
time.sleep(2)
|
||||
try:
|
||||
o = self.client.get_order(order_id=order_id)
|
||||
if float(o.size_matched or 0) > 0:
|
||||
px = float(o.price or price)
|
||||
if o.status not in ("live", "delayed"):
|
||||
break
|
||||
except Exception: # gone from the open view — terminal
|
||||
break
|
||||
try:
|
||||
if order_id:
|
||||
self.client.cancel_order(order_id=order_id)
|
||||
else: # exception path: sweep the whole token
|
||||
ids = [o.id for o in self.client.list_open_orders(
|
||||
token_id=str(token_id))]
|
||||
if ids:
|
||||
self.client.cancel_orders(order_ids=ids)
|
||||
except Exception:
|
||||
pass # cancel of a just-matched order — fine
|
||||
try:
|
||||
bal1 = self._shares_held(token_id)
|
||||
except Exception:
|
||||
return 0.0, price # can't read truth: claim nothing; the
|
||||
# chain_cash_gap alarm catches the rest
|
||||
filled = (bal1 - bal0) if side == "BUY" else (bal0 - bal1)
|
||||
return max(filled, 0.0), px
|
||||
|
||||
def _order(self, token_id, shares, price, side):
|
||||
import math
|
||||
sz = math.floor(shares * 100) / 100.0 # cost never exceeds the gated stake
|
||||
try:
|
||||
bal0 = self._shares_held(token_id)
|
||||
except Exception as e: # no truth anchor → refuse to place at all
|
||||
return {"ok": False, "filled_shares": 0.0, "price": price,
|
||||
"resp": f"pre-check failed: {e}", "paper": False}
|
||||
try:
|
||||
if side == "BUY":
|
||||
r = self.client.place_market_order(
|
||||
@@ -293,7 +344,13 @@ class LedgerLiveExecutor:
|
||||
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 —
|
||||
# but a timed-out post may still be resting: sweep + measure first
|
||||
filled, px = self._settle_uncertain(token_id, side, bal0, price)
|
||||
if filled > 0:
|
||||
return {"ok": True, "filled_shares": filled, "price": px,
|
||||
"resp": f"filled despite client error: {e}",
|
||||
"paper": False}
|
||||
return {"ok": False, "filled_shares": 0.0, "price": price,
|
||||
"resp": f"exception: {e}", "paper": False}
|
||||
if not getattr(r, "ok", False): # RejectedOrder: typed code + message
|
||||
@@ -303,8 +360,14 @@ class LedgerLiveExecutor:
|
||||
making = float(r.making_amount or 0) # what we gave (matched)
|
||||
taking = float(r.taking_amount or 0) # what we got (matched)
|
||||
filled, usd = (taking, making) if side == "BUY" else (making, taking)
|
||||
return {"ok": filled > 0, "filled_shares": filled,
|
||||
"price": usd / filled if filled else price,
|
||||
px = usd / filled if filled else price
|
||||
if filled <= 0:
|
||||
# ACCEPTED with zero matched = in-play 'delayed'/'live' hold, NOT
|
||||
# a rejection (the 2026-07-10 lesson). Wait it out briefly, then
|
||||
# cancel-and-measure so the ledger always matches the exchange.
|
||||
filled, px = self._settle_uncertain(token_id, side, bal0, price,
|
||||
order_id=r.order_id)
|
||||
return {"ok": filled > 0, "filled_shares": filled, "price": px,
|
||||
"resp": {"order_id": r.order_id, "status": r.status,
|
||||
"making": making, "taking": taking,
|
||||
"trades": len(r.trade_ids)},
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
{"ts": 1783665419.1, "wallet": "0xbadaf319415c17f28824a43ae0cd912b9d84d874", "token": "72380260523779452108747496691590035418744958761813904753644512394906005337193", "name": "0xbadaf319", "outcome": "No", "title": "Will LAB reach $24 before 2027?", "detect_lag_s": 268.1, "their_price": 0.95, "my_price": 0.95, "slippage_pct": -0.0, "shares": 5.26, "cost": 5.0, "fee": 0.0075, "mode": "live", "book": {"bb": 0.91, "ba": 0.95, "spread": 0.04, "bid5c": 58.89, "ask5c": 40.98}}
|
||||
{"ts": 1783674610, "wallet": "0x1d0034ac09da54ba7ec969d1eb756b0a1df475c5", "token": "1161663761026248928741317236361093152095776065309269380062540556077381245572", "name": "AIcAIc", "outcome": "G2 Esports", "title": "LoL: G2 Esports vs LYON - Game 2 Winner", "detect_lag_s": 6.0, "their_price": 0.64, "my_price": 0.64, "slippage_pct": 0.0, "shares": 7.8125, "cost": 5.0, "fee": 0.0844, "mode": "live", "note": "reconstructed from chain \u2014 delayed-fill incident"}
|
||||
{"ts": 1783675741, "wallet": "", "token": "reconstructed-1783675741", "name": "(incident)", "outcome": "Rune Eaters", "title": "Dota 2: Rune Eaters vs GamerLegion - Game 1 Winner", "detect_lag_s": 6.0, "their_price": 0.4, "my_price": 0.4, "slippage_pct": 0.0, "shares": 12.5, "cost": 5.0, "fee": 0.15, "mode": "live", "note": "reconstructed from chain \u2014 delayed-fill incident"}
|
||||
{"ts": 1783678196, "wallet": "0x1d0034ac09da54ba7ec969d1eb756b0a1df475c5", "token": "47405794293316148244590035888323531202770006061275899647071095856237793879763", "name": "AIcAIc", "outcome": "G2 Esports", "title": "LoL: G2 Esports vs LYON - Game 3 Winner", "detect_lag_s": 6.0, "their_price": 0.39, "my_price": 0.39, "slippage_pct": 0.0, "shares": 12.8205, "cost": 5.0, "fee": 0.15, "mode": "live", "note": "reconstructed from chain \u2014 delayed-fill incident"}
|
||||
{"ts": 1783685626, "wallet": "", "token": "reconstructed-1783685626", "name": "(incident)", "outcome": "PlayTime", "title": "Dota 2: Aurora vs PlayTime - Game 1 Winner", "detect_lag_s": 6.0, "their_price": 0.45, "my_price": 0.45, "slippage_pct": 0.0, "shares": 11.1111, "cost": 5.0, "fee": 0.15, "mode": "live", "note": "reconstructed from chain \u2014 delayed-fill incident"}
|
||||
{"ts": 1783686500, "wallet": "", "token": "reconstructed-1783686500", "name": "(incident)", "outcome": "Aurora", "title": "Dota 2: Aurora vs PlayTime - Game 1 Winner", "detect_lag_s": 6.0, "their_price": 0.57, "my_price": 0.57, "slippage_pct": 0.0, "shares": 8.7719, "cost": 5.0, "fee": 0.1132, "mode": "live", "note": "reconstructed from chain \u2014 delayed-fill incident"}
|
||||
{"ts": 1783689280, "wallet": "0xd9fa6d54cf294343ee42bbb735b7cff20a4bf0b9", "token": "49886282303318964434544606851164664333698127985256361511182784657423645654000", "name": "gkmgkldfmg", "outcome": "Aurora", "title": "Dota 2: Aurora vs PlayTime - Game 2 Winner", "detect_lag_s": 6.0, "their_price": 0.69, "my_price": 0.69, "slippage_pct": 0.0, "shares": 7.2464, "cost": 5.0, "fee": 0.0674, "mode": "live", "note": "reconstructed from chain \u2014 delayed-fill incident"}
|
||||
|
||||
+5773
-5722
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user