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.