Skip to main content

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.