Accept paper MOC orders IBKR holds as PendingSubmit; stop retrying after the order window

On 2026-10-13 the 09:53 ET run placed six MOC orders and failed: the paper
account reports MOC orders as PendingSubmit until the closing auction and sent
no acknowledgement within 30 s, so confirm_orders treated them as rejected.
IBKR listed all six with permanent ids and filled them at the close, as it did
the 14 PendingSubmit orders of 2026-09-29. Every retry then stopped at "6 open
orders already exist", and after 15:45 ET at the order window, and the start
limit's 6-hour window reset at 15:50 ET, so the job restarted every 10 minutes
into the evening (42 restarts by 17:28 ET).

- confirm_orders accepts a PendingSubmit order once IBKR lists it with a
  permanent id, re-reading this client's open orders every 5 s for up to 60 s.
- rebalance --submit succeeds without sending anything when every open order
  comes from today's runs and IBKR still holds every order those runs sent;
  a missing one still fails.
- The trade unit skips starts after 15:45 ET (ExecCondition), which systemd
  does not restart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 14:30:41 -07:00
parent e55c4ffbdb
commit 35d750f0a5
4 changed files with 121 additions and 9 deletions

View File

@@ -190,6 +190,17 @@ async def rebalance(cfg: Config, submit: bool) -> None:
raise SystemExit(f"{now:%H:%M} ET is outside the order window {ORDER_WINDOW[0]}-{ORDER_WINDOW[1]}")
if open_trades := await broker.open_trades():
# A run whose confirmation timed out exits non-zero and the trade service retries it;
# if IBKR holds every order that run sent, the session's orders are in.
refs = {t.order.orderRef for t in open_trades}
sent = [cfg.root / "runs" / ref.removeprefix("ibkr-quant ") / "orders.csv" for ref in refs]
if all(ref.startswith(f"ibkr-quant {now:%Y%m%d}-") for ref in refs) and all(p.exists() for p in sent):
sent_ids = set(pd.concat(pd.read_csv(p) for p in sent)["order_id"])
if missing := sent_ids - {t.order.orderId for t in open_trades}:
raise SystemExit(f"today's orders {sorted(missing)} are no longer open at IBKR "
"(rejected, cancelled or filled); see `make check`")
print(f"{len(open_trades)} orders from {', '.join(sorted(refs))} are open at IBKR; not submitting again")
return
raise SystemExit(f"{len(open_trades)} open orders already exist; cancel or let them finish first")
contracts, unknown = await broker.qualify_stocks([o.symbol for o in orders])
if unknown: