fix: re-query order after cancel to catch late in-play fills
Root cause of recurring CASH≠CHAIN drift: in-play orders can fill in the window between cancel_order and the get_order status check. The exchange reports size_matched=0 (order was just cancelled) but the fill already hit the chain, so the bot marks it 'pending expired unfilled' while the position actually exists on-chain. Fix: after the initial unfilled determination, re-query the order one more time. If size_matched is now > 0, adopt it as a fill instead of recording a miss. This prevents the phantom cash gap that required manual state clearing on every in-play fill. ponytail: adds one extra get_order call per expired pending (only when the first check says unfilled). No impact on the normal fill path.
This commit is contained in:
+20
-1
@@ -1499,7 +1499,7 @@ class Copybot:
|
|||||||
sz_cap = p.get("sz") or p.get("shares_req") or float("inf")
|
sz_cap = p.get("sz") or p.get("shares_req") or float("inf")
|
||||||
filled = min(matched, sz_cap)
|
filled = min(matched, sz_cap)
|
||||||
if filled <= 0 and not order_view:
|
if filled <= 0 and not order_view:
|
||||||
# the exchange no longer knows the order — ONLY then is the
|
# the exchange no longer knows the order - ONLY then is the
|
||||||
# balance diff worth consulting (a definitive killed/0 answer
|
# balance diff worth consulting (a definitive killed/0 answer
|
||||||
# above means unfilled, full stop)
|
# above means unfilled, full stop)
|
||||||
try:
|
try:
|
||||||
@@ -1509,6 +1509,25 @@ class Copybot:
|
|||||||
filled = min(max(diff, 0.0), sz_cap)
|
filled = min(max(diff, 0.0), sz_cap)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
# ponytail: in-play orders can fill between the cancel and the
|
||||||
|
# status check above - the exchange reports 0 matched but the
|
||||||
|
# fill already hit the chain. Re-query the order ONE more time
|
||||||
|
# after cancel settles; if matched is still 0 but our cost was
|
||||||
|
# deducted from chain pUSD, adopt it as a fill (the CASH≠CHAIN
|
||||||
|
# alarm is the symptom this prevents). Ceiling: this adds one
|
||||||
|
# extra get_order + one get_balance_allowance per expired pending.
|
||||||
|
if filled <= 0.01 and p.get("order_id"):
|
||||||
|
try:
|
||||||
|
o2 = ex.client.get_order(order_id=p["order_id"])
|
||||||
|
m2 = float(o2.size_matched or 0)
|
||||||
|
if m2 > 0:
|
||||||
|
filled = min(m2, sz_cap)
|
||||||
|
px = float(o2.price or px)
|
||||||
|
log(f"pending adopted late fill: {p['outcome']} · "
|
||||||
|
f"{p['title'][:40]} - {filled:.2f} sh @ {px:.3f} "
|
||||||
|
f"(matched after cancel)")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
if filled <= 0.01:
|
if filled <= 0.01:
|
||||||
log(f"pending expired unfilled: {p['outcome']} · {p['title'][:40]}")
|
log(f"pending expired unfilled: {p['outcome']} · {p['title'][:40]}")
|
||||||
if p["side"] == "BUY" and not p.get("is_add"):
|
if p["side"] == "BUY" and not p.get("is_add"):
|
||||||
|
|||||||
Reference in New Issue
Block a user