Confirm MOC orders were accepted before reporting success

rebalance used to sleep 3 s and record whatever status TWS had sent; on
2026-09-29 every order was still PendingSubmit with permId 0. Now it waits
up to 30 s for PreSubmitted/Submitted/Filled, records the final statuses in
orders.csv either way, and exits non-zero naming any rejected or
unacknowledged order so the systemd unit's failure is visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 05:15:53 -07:00
parent eaa77ef187
commit a87a0c3b54
3 changed files with 112 additions and 3 deletions

View File

@@ -4,7 +4,7 @@ from dataclasses import dataclass
from zoneinfo import ZoneInfo
import pandas as pd
from ib_async import IB, Contract, LimitOrder, Order, Stock, Trade
from ib_async import IB, Contract, LimitOrder, Order, OrderStatus, Stock, Trade
from .config import IBConfig
@@ -170,6 +170,25 @@ class Broker:
order.orderRef = ref
return self.ib.placeOrder(contract, order)
async def confirm_orders(self, trades: list[Trade], timeout: float = 30) -> None:
accepted = {OrderStatus.PreSubmitted, OrderStatus.Submitted, OrderStatus.Filled}
terminal = accepted | OrderStatus.DoneStates
loop = asyncio.get_running_loop()
deadline = loop.time() + timeout
while any(t.orderStatus.status not in terminal for t in trades):
remaining = deadline - loop.time()
if remaining <= 0:
break
await asyncio.sleep(min(0.1, remaining))
failed = [t for t in trades if t.orderStatus.status not in accepted]
if failed:
details = []
for trade in failed:
errors = "; ".join(entry.message for entry in trade.log if entry.errorCode)
details.append(f"{trade.contract.symbol} order {trade.order.orderId}: {trade.orderStatus.status}"
+ (f" ({errors})" if errors else ""))
raise RuntimeError("orders not accepted (rejected or confirmation timed out): " + "; ".join(details))
async def executions(self) -> pd.DataFrame:
fills = await self.ib.reqExecutionsAsync()
return pd.DataFrame(

View File

@@ -195,7 +195,11 @@ async def rebalance(cfg: Config, submit: bool) -> None:
broker.place(contracts[o.symbol], o.delta, ex.order_type, limits.get(o.symbol), f"ibkr-quant {run_dir.name}")
for o in orders
]
await asyncio.sleep(3) # let TWS acknowledge before recording status
confirmation_error = None
try:
await broker.confirm_orders(trades)
except RuntimeError as exc:
confirmation_error = exc
submitted = pd.DataFrame(
[
{
@@ -213,7 +217,9 @@ async def rebalance(cfg: Config, submit: bool) -> None:
)
submitted.to_csv(run_dir / "orders.csv", index=False)
print(submitted.to_string(index=False))
print(f"submitted {len(trades)} orders -> {run_dir}")
if confirmation_error:
raise SystemExit(f"{confirmation_error}; recorded status -> {run_dir}")
print(f"accepted {len(trades)} orders -> {run_dir}")
def run_backtest(cfg: Config, top_ns: list[int], everies: list[int], capitals: list[float], fee: str) -> None: