From 7662d82232fa9146e64a2070ca5ef2faa5f7cdee Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Mon, 8 Jun 2026 00:34:01 +0200 Subject: [PATCH] fix(bot): whole-share orders (IBKR API rejects fractional, err 10243) + state only on accepted Eyes-on EXECUTE=true validation caught two bugs: (1) fractional qty -> all 6 orders cancelled (10243); round to whole shares (skip if 0). (2) last_rebalance was set even when all orders were rejected, which would block retry; now only marked when >=1 order is accepted. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/surfer/multistrat_bot.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/surfer/multistrat_bot.py b/scripts/surfer/multistrat_bot.py index 731564b84..320fd0462 100644 --- a/scripts/surfer/multistrat_bot.py +++ b/scripts/surfer/multistrat_bot.py @@ -160,7 +160,11 @@ def main(): continue if val > C["max_order"] * nlv: log("WARN", "order_capped", ticker=t, value=round(val), cap=round(C["max_order"] * nlv)) - orders.append((t, "BUY" if delta > 0 else "SELL", round(abs(delta), 4), round(val))) + qty = int(round(abs(delta))) # whole shares — IBKR API rejects fractional (error 10243) + if qty == 0: + log("INFO", "skip_subshare", ticker=t, note="rounds to 0 whole shares") + continue + orders.append((t, "BUY" if delta > 0 else "SELL", qty, round(val))) for t, side, qty, val in orders: log("INFO", "planned_order", ticker=t, side=side, qty=qty, value=val) if not orders: @@ -183,10 +187,14 @@ def main(): except Exception as e: log("ERROR", "order_failed", ticker=t, err=str(e)[:120]) # continue — partial book is recoverable next run ib.sleep(3) + accepted = sum(1 for tr in placed if tr.orderStatus.status not in ("Cancelled", "ApiCancelled", "Inactive")) filled = sum(1 for tr in placed if tr.orderStatus.status == "Filled") newpos = {p.contract.symbol: p.position for p in ib.positions()} - state["last_rebalance"] = dt.date.today().isoformat(); state["rebalances"] = state.get("rebalances", 0) + 1 - log("INFO", "reconcile", placed=len(placed), filled=filled, positions=newpos or None) + if accepted > 0: # mark the period done only if orders were actually accepted (not all rejected) + state["last_rebalance"] = dt.date.today().isoformat(); state["rebalances"] = state.get("rebalances", 0) + 1 + else: + log("WARN", "no_orders_accepted", note="all rejected — not marking rebalance; will retry next run") + log("INFO", "reconcile", placed=len(placed), accepted=accepted, filled=filled, positions=newpos or None) return 0 finally: save_state(state)