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) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-06-08 00:34:01 +02:00
parent 2a468e4b5e
commit 7662d82232

View File

@@ -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)