Async state model and events
The central IB object is both a request facade and an in-memory projection. Incoming protocol messages mutate Trade, Ticker, account, portfolio, bar, and fill objects in place, then emit events.
The one event-loop rule
Do not block the event loop. time.sleep, CPU-heavy calculations, synchronous network clients, and large dataframe work prevent socket messages from being decoded. Consequences include stale ticks, delayed order-state handling, request timeouts, and a growing receive buffer.
# Correct delay
await ib.sleep(1)
# CPU work
result = await asyncio.to_thread(expensive_function, payload)
IB.sleep(0) yields so pending messages can be processed. Any live object may change during that yield.
Snapshot accessors versus requests
Methods such as positions(), openTrades(), fills(), tickers(), and accountValues() read the current local projection. They do not make a network round trip. Methods prefixed with req normally issue a request or create a subscription. Prefer the synchronized accessors after a successful startup sync.
reqOpenOrders() is a notable trap: the library warns that its returned snapshot can be stale. openTrades() and openOrders() are faster and continuously maintained for orders visible to this client.
Events
def on_status(trade):
print(trade.order.permId, trade.orderStatus.status)
ib.orderStatusEvent += on_status
Key events include connectedEvent, disconnectedEvent, pendingTickersEvent, barUpdateEvent, newOrderEvent, orderModifyEvent, cancelOrderEvent, openOrderEvent, orderStatusEvent, execDetailsEvent, commissionReportEvent, account/portfolio/PnL events, news/scanner events, errorEvent, and timeoutEvent.
Avoid recursively issuing many requests inside callbacks. Schedule substantial work with asyncio.create_task, apply backpressure, and make handlers idempotent because duplicated or repeated status callbacks are possible.
Timeouts
IB.RequestTimeout affects blocking requests only; the default 0 waits indefinitely. Async methods must be wrapped explicitly:
bars = await asyncio.wait_for(
ib.reqHistoricalDataAsync(contract, "", "1 D", "5 mins", "TRADES", True),
timeout=30,
)
Cancellation or timeout is not proof that IBKR did not perform an action. This matters most for order placement: placeOrder returns a Trade immediately and is not an acknowledgement of exchange acceptance.
IB.setTimeout(seconds) detects absence of all inbound traffic. It emits once per connected session until reset. Quiet traffic is not necessarily a dead connection, so production health checks should combine socket state, error codes, last-message age, and a bounded probe.
Error policy
With IB.RaiseRequestErrors = False, some failed requests resolve to an empty result and emit errorEvent. With True, request-specific failures raise RequestError. Always subscribe to errorEvent; warning/informational codes share that channel with fatal errors.