diff --git a/copybot.py b/copybot.py index 7093bba5..02cbd27e 100644 --- a/copybot.py +++ b/copybot.py @@ -1499,7 +1499,7 @@ class Copybot: sz_cap = p.get("sz") or p.get("shares_req") or float("inf") filled = min(matched, sz_cap) 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 # above means unfilled, full stop) try: @@ -1509,6 +1509,25 @@ class Copybot: filled = min(max(diff, 0.0), sz_cap) except Exception: 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: log(f"pending expired unfilled: {p['outcome']} · {p['title'][:40]}") if p["side"] == "BUY" and not p.get("is_add"):