Lesson 4.4.2 · 70 min

Interactive Brokers: TWS and IB Gateway, ib_async, contracts, data and orders

The Interactive Brokers API does not talk to its servers but to a local program that has to be kept alive. This lesson covers what you need to trade with it from Python: choosing between TWS and IB Gateway, living with their restarts, qualifying contracts, requesting data knowing what type it is, sending bracket orders without leaving a position without a stop and rebuilding state after a disconnection. The real ib_async code is not executed; a simulated IB with a logical clock reproduces, message by message, a real order from the course data.

Fig. — Order R00005 (market buy of 434 ORSC) message by message in the simulated IB: statuses, executions and commission reports, in milliseconds since placeOrder
Level 4 lessons
  1. 4.1 · Portfolio theory and optimisation

    1. 4.1.1 Markowitz: efficient frontier and real constraints
    2. 4.1.2 Covariance estimation: shrinkage, factor models and denoising
    3. 4.1.3 Black-Litterman: from market equilibrium to views
    4. 4.1.4 Risk parity, minimum variance, HRP and HERC
    5. 4.1.5 Optimization with CVaR, drawdown, turnover and costs
    6. 4.1.6 Factor models: CAPM, Fama-French, PCA and attribution
    7. 4.1.7 Rebalancing and strategy portfolios
    8. 4.1.8 Portfolio tools: Riskfolio-Lib, PyPortfolioOpt, skfolio and bt
  2. 4.2 · Risk management and position sizing

    1. 4.2.1 Risk per trade, stops and risk of ruin
    2. 4.2.2 Kelly criterion and volatility targeting: how much to risk
    3. 4.2.3 VaR, Expected Shortfall and VaR backtesting
    4. 4.2.4 Stress testing and correlation, liquidity, gap and counterparty risk
    5. 4.2.5 Drawdown control and kill switch
    6. 4.2.6 Derivatives risk: portfolio Greeks, margin and liquidations
  3. 4.3 · Microstructure and execution

    1. 4.3.1 Implementation shortfall, VWAP, TWAP and Almgren-Chriss
    2. 4.3.2 Order flow imbalance, microstructure signals and latency
  4. 4.4 · Brokers and APIs

    1. 4.4.1 Choosing a broker from Spain or the EU: regulation, assets, costs and API
    2. 4.4.2 Interactive Brokers: TWS and IB Gateway, ib_async, contracts, data and orders
    3. 4.4.3 Alpaca, Tradier, MetaTrader 5 with Python and OANDA v20
    4. 4.4.4 Crypto exchange APIs: ccxt, Binance, Bybit, Kraken and Coinbase
    5. 4.4.5 Secure credentials: environment variables, secret managers and least privilege
  5. 4.5 · Live trading system

    1. 4.5.1 Live system architecture: from data to reconciliation
    2. 4.5.2 Order management: states, partial fills, cancellations and reconnections
    3. 4.5.3 Paper trading, shadow trading and slippage monitoring
    4. 4.5.4 Task scheduling and alerts: cron, APScheduler, Prefect, Telegram, Slack and email
    5. 4.5.5 Fault tolerance: idempotency, persistent state and reconciliation at startup
    6. 4.5.6 NautilusTrader or LEAN live: from backtest to production with the same code

What has been covered and what is missing

Lesson 1.3.6 presented the architecture of the Interactive Brokers (IBKR) API: the program does not talk to the broker's servers but to TWS or IB Gateway, which run on the machine itself and open a local port (7496 and 7497 in TWS, 4001 and 4002 in Gateway, live and paper), and it downloaded history with ib_async respecting the pacing limits. Lesson 3.3.3 sent a currency order with Forex, LimitOrder, whatIfOrder and orderRef. This lesson completes what you need to run a system: the life cycle of TWS and Gateway, the ib_async programming model, contracts, market data types, bracket orders, events and reconnection.

Lesson 4.4.1 left a task to do first: check which entity you sign with. Interactive Brokers Ireland Limited appears in the CNMV list of EEA investment firms under freedom to provide services (registered on December 29, 2020) and in the ESMA register as an active investment firm, authorized by the Central Bank of Ireland (queries of October 22, 2026); the group company you sign with is stated in the contract, and that is the one to look up. The method of that lesson works for checking this broker and any other.

TWS or IB Gateway

According to the TWS API documentation, from a program's point of view IB Gateway and TWS are identical: both are the server the socket is opened to after the user has authenticated, and in both you have to type a username and password in a login window (a session without a graphical interface is not supported). Gateway is lighter (it uses around 40% fewer resources); TWS adds the platform's graphical tools1Note 1 · Interactive Brokers, TWS API, The IB Gateway, accessed on October 22, 2026..

TWSIB Gateway
Default ports (live / paper)7496 / 74974001 / 4002
InterfaceFull platformMinimal window
ResourcesMoreAround 40% fewer
Typical useDevelopment, manual monitoringServer for an automated system

What constrains a system most is not the choice but the life cycle they share:

  • Daily restart. Both are designed to restart every day (among other things, to download the contract definitions again). Since version 974 they have an automatic restart with no intervention; with it they can run from Sunday to Sunday without authenticating again, and after the Saturday night server reset the credentials have to be entered again1. The reauthentication page recommends that API users enable the “Never lock Trader Workstation” and “Auto restart” options, and explains the message that appears when the weekly reauthentication is due2Note 2 · Interactive Brokers, TWS API, Daily & Weekly Reauthentication, accessed on October 22, 2026..
  • Server maintenance. The system codes documentation warns that it is very likely you will lose the connection to the servers at least once a day because of daily maintenance, that TWS or Gateway reconnect by themselves afterwards and that trading during those windows is not recommended3Note 3 · Interactive Brokers, TWS API, System Message Codes (1100, 1101, 1102 and 1300), accessed on October 22, 2026..
  • Automatic login. Since the session requires a window, unattended systems have used third-party tools that fill in the username and password and manage the restarts. The best known, IBC, on which the ib_async Watchdog class relies, announced its retirement on September 1, 2026: its repository is archived, with no new features or active support4Note 4 · IBC, repository README (“This project has been retired, as of 1 September 2026”); ib_async 2.1.0, Watchdog class (source code), accessed on October 22, 2026.. Whoever uses it inherits that maintenance risk.
error 11001102 data intact1101 data lostdaily restartreconnect and reconcileConnectedNoServerResubscribeNoSocket
Mermaid source
stateDiagram-v2
  [*] --> Connected
  Connected --> NoServer: error 1100
  NoServer --> Connected: 1102 data intact
  NoServer --> Resubscribe: 1101 data lost
  Resubscribe --> Connected
  Connected --> NoSocket: daily restart
  NoSocket --> Connected: reconnect and reconcile

Messages 1100 (the connection between TWS and the servers was lost), 1101 (restored, with the data subscriptions lost: they have to be requested again) and 1102 (restored, with the data maintained) arrive as errors, but they are not the consequence of anything the program did3. When it is TWS or Gateway itself that goes down, the socket is lost: ib_async emits disconnectedEvent and, on reconnecting, resynchronizes its state. The program has to rebuild its own: the last section does that.

ib_async 2.1.0: the programming model

ib_async is the continuation of ib_insync (lesson 1.3.6). Version 2.1.0 was published on PyPI on December 8, 2025 and was the latest on October 22, 2026; it requires Python 3.10 or later and depends on aeventkit, nest_asyncio and tzdata (with the constraint tzdata<2026.0)5Note 5 · ib_async on PyPI (version 2.1.0, published on December 8, 2025; dependencies aeventkit>=2.1.0,<3.0.0, nest_asyncio and tzdata>=2025.2,<2026.0) and API documentation, accessed on October 22, 2026.. It is not installed in the course environment: with the environment's tzdata version (2026.5), adding it would force a tzdata downgrade, and running it requires a TWS or Gateway session. That is why the real code goes in blocks that are not executed and, to run something, the lesson builds a simulated IB with the same shape.

IN [01]python
import importlib.util
import sys

print("Python", sys.version.split()[0])
print("ib_async installed:", importlib.util.find_spec("ib_async")
      is not None)
OUT [01]Is ib_async in the environment?
Python 3.14.9
ib_async installed: False

The real code for a connection, as documented by version 2.1.0:

IN [02]ib_conexion.pypython
# uv add ib_async   (2.1.0)
from ib_async import IB, util

ib = IB()
ib.connect("127.0.0.1", 4002, clientId=7,    # IB Gateway, paper
           timeout=10, readonly=False)
print(ib.isConnected(), ib.managedAccounts())
print(ib.positions())                         # already synchronized
ib.sleep(1)                                   # never time.sleep
ib.disconnect()
# In Jupyter, before anything else: util.startLoop()

Three ideas hold the library up:

  • A synchronous API on top of asyncio. connect, qualifyContracts or reqExecutions block until they have the response, but underneath there is an asyncio loop that keeps processing messages. While the program waits, that loop has to run: that is why you wait with ib.sleep() or ib.run() and never with time.sleep(), which freezes it (the util.sleep documentation itself says so). Each method has its …Async version for use with await.
  • Live objects. placeOrder returns a Trade and reqMktData a Ticker, which update themselves as messages arrive: trade.orderStatus.status, trade.fills, ticker.bid.
  • Events. The IB object and each Trade expose events (orderStatusEvent, execDetailsEvent, commissionReportEvent, errorEvent, disconnectedEvent; trade.filledEvent, trade.fillEvent…) to which a function is connected with +=. The documentation advises against making new requests inside an event handler.

The simulator reproduces those three ideas with the same classes and fields as version 2.1.0 (checked in its source code): Contract, Order, OrderStatus, Execution, CommissionReport, Fill, Trade and a minimal Event.

IN [03]python
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import NamedTuple

class Event:
    """The essentials of eventkit.Event: += connects, emit calls."""
    def __init__(self, nombre):
        self.nombre, self.handlers = nombre, []
    def __iadd__(self, f):
        self.handlers.append(f)
        return self
    def emit(self, *args):
        for f in list(self.handlers):
            f(*args)

@dataclass
class Contract:
    secType: str = ""
    conId: int = 0
    symbol: str = ""
    lastTradeDateOrContractMonth: str = ""
    multiplier: str = ""
    exchange: str = ""
    primaryExchange: str = ""
    currency: str = ""
    localSymbol: str = ""

def Stock(symbol="", exchange="", currency="", **kw):
    return Contract("STK", symbol=symbol, exchange=exchange,
                    currency=currency, **kw)

@dataclass
class Order:
    orderId: int = 0
    clientId: int = 0
    action: str = ""
    totalQuantity: float = 0.0
    orderType: str = ""
    lmtPrice: float = 0.0
    auxPrice: float = 0.0
    tif: str = ""
    orderRef: str = ""
    transmit: bool = True
    parentId: int = 0

def MarketOrder(action, q, **kw):
    return Order(action=action, totalQuantity=q, orderType="MKT", **kw)
def LimitOrder(action, q, lmt, **kw):
    return Order(action=action, totalQuantity=q, orderType="LMT",
                 lmtPrice=lmt, **kw)
def StopOrder(action, q, stop, **kw):
    return Order(action=action, totalQuantity=q, orderType="STP",
                 auxPrice=stop, **kw)

@dataclass
class OrderStatus:
    orderId: int = 0
    status: str = ""
    filled: float = 0.0
    remaining: float = 0.0
    avgFillPrice: float = 0.0
    permId: int = 0
    lastFillPrice: float = 0.0
    DoneStates = frozenset(["Filled", "Cancelled", "ApiCancelled",
                            "Inactive"])

@dataclass
class Execution:
    execId: str = ""
    time: datetime = None
    acctNumber: str = ""
    exchange: str = ""
    side: str = ""
    shares: float = 0.0
    price: float = 0.0
    permId: int = 0
    orderId: int = 0
    cumQty: float = 0.0
    avgPrice: float = 0.0
    orderRef: str = ""

@dataclass
class CommissionReport:
    execId: str = ""
    commission: float = 0.0
    currency: str = ""
    realizedPNL: float = 0.0

class Fill(NamedTuple):
    contract: Contract
    execution: Execution
    commissionReport: CommissionReport
    time: datetime

@dataclass
class Trade:
    contract: Contract
    order: Order
    orderStatus: OrderStatus
    fills: list = field(default_factory=list)
    log: list = field(default_factory=list)
    def __post_init__(self):
        for n in ["statusEvent", "fillEvent", "filledEvent"]:
            setattr(self, n, Event(n))
    def isDone(self):
        return self.orderStatus.status in OrderStatus.DoneStates

print([f for f in Execution.__dataclass_fields__][:7])
OUT [03]The pieces of ib_async, in miniature
['execId', 'time', 'acctNumber', 'exchange', 'side', 'shares', 'price']

They are subsets of the real classes (which have many more fields), with the same names. The simulated IB keeps a logical clock: messages are scheduled with a delay in milliseconds and sleep delivers them in order, without looking at the system clock or really sleeping, so the output is always the same. Its public methods have the same signature as those of ib_async (connect, qualifyContracts, reqMarketDataType, reqMktData, placeOrder, sleep, reqExecutions); the ones that start with _ play the part of TWS and the broker.

IN [04]python
import heapq

@dataclass
class Ticker:
    contract: Contract
    marketDataType: int = 1
    bid: float = float("nan")
    ask: float = float("nan")
    def hasBidAsk(self):
        return self.bid == self.bid and self.ask == self.ask
    def midpoint(self):
        return ((self.bid + self.ask) / 2 if self.hasBidAsk()
                else float("nan"))

class IBSimulado:
    """The ib_async.IB interface without a network: a logical clock and
    scheduled messages like the ones TWS or IB Gateway would send."""
    EVENTOS = ["connectedEvent", "disconnectedEvent", "errorEvent",
               "orderStatusEvent", "execDetailsEvent",
               "commissionReportEvent"]

    def __init__(self, catalogo, t0, suscritos=(), cotizaciones=None):
        self.catalogo, self.t, self.t0 = catalogo, t0, t0
        self.suscritos = set(suscritos)
        self.cotizaciones = cotizaciones or {}
        self.cola, self.n, self.retenidas = [], 0, []
        self.trades, self._fills, self.broker_fills = {}, {}, []
        self.next_id, self.tipo_datos, self.conectado = 1, 1, False
        for e in self.EVENTOS:
            setattr(self, e, Event(e))

    # logical clock
    def programa(self, dt_ms, f, *args):
        self.n += 1
        cuando = self.t + timedelta(milliseconds=dt_ms)
        heapq.heappush(self.cola, (cuando, self.n, f, args))
    def sleep(self, segundos):
        fin = self.t + timedelta(seconds=segundos)
        while self.cola and self.cola[0][0] <= fin:
            self.t, _, f, args = heapq.heappop(self.cola)
            f(*args)
        self.t = fin
    def ms(self):
        return (self.t - self.t0) / timedelta(milliseconds=1)

    def connect(self, host="127.0.0.1", port=4002, clientId=1):
        self.clientId, self.conectado = clientId, True
        self.connectedEvent.emit()

    # contracts, like qualifyContracts in version 2.1.0
    def qualifyContracts(self, *contratos):
        salida = []
        for c in contratos:
            pedidos = {k: v for k, v in vars(c).items()
                       if v and k != "exchange"}
            cand = [d for d in self.catalogo
                    if all(self._igual(d, k, v)
                           for k, v in pedidos.items())]
            if len(cand) != 1:
                if not cand:
                    self.errorEvent.emit(-1, 200, "No security "
                        "definition has been found for the request", c)
                salida.append(None)
                continue
            destino = c.exchange
            for k, v in vars(cand[0]).items():
                setattr(c, k, v)
            c.exchange = destino or cand[0].exchange
            salida.append(c)
        return salida
    @staticmethod
    def _igual(d, k, v):
        if k == "lastTradeDateOrContractMonth":   # YYYYMM or YYYYMMDD
            return getattr(d, k).startswith(v)
        return getattr(d, k) == v

    # market data
    def reqMarketDataType(self, tipo):
        self.tipo_datos = tipo
    def reqMktData(self, contrato):
        tk = Ticker(contrato, marketDataType=self.tipo_datos)
        if (self.tipo_datos in (1, 2)
                and contrato.conId not in self.suscritos):
            self.programa(5, self.errorEvent.emit, 1, 354,
                          "Requested market data is not subscribed",
                          contrato)
            return tk
        for dt, bid, ask in self.cotizaciones.get(contrato.conId, []):
            self.programa(dt, self._tick, tk, bid, ask)
        return tk
    def _tick(self, tk, bid, ask):
        tk.bid, tk.ask = bid, ask

    # orders
    def placeOrder(self, contrato, orden):
        if not orden.orderId:
            orden.orderId, self.next_id = self.next_id, self.next_id + 1
        orden.clientId = self.clientId
        tr = Trade(contrato, orden, OrderStatus(
            orderId=orden.orderId, status="PendingSubmit",
            remaining=orden.totalQuantity))
        self.trades[orden.orderId] = tr
        tr.log.append((self.ms(), "PendingSubmit"))
        if not orden.transmit:          # created in TWS, not sent
            self.retenidas.append(tr)
            return tr
        grupo = [t for t in self.retenidas
                 if orden.parentId in (t.order.orderId,
                                       t.order.parentId)]
        for t in grupo + [tr]:
            hija = t.order.parentId != 0
            self.programa(30, self._status, t, {
                "status": "PreSubmitted" if hija else "Submitted"})
        self.retenidas = [t for t in self.retenidas if t not in grupo]
        return tr

    # what TWS and the broker do
    def _status(self, tr, cambios):
        if tr.isDone():                  # a final status does not change
            return
        for k, v in cambios.items():
            setattr(tr.orderStatus, k, v)
        tr.log.append((self.ms(), tr.orderStatus.status))
        self.orderStatusEvent.emit(tr)
        tr.statusEvent.emit(tr)
        if tr.orderStatus.status == "Filled":
            tr.filledEvent.emit(tr)
    def _exec(self, tr, ex, cr):
        self.broker_fills.append(Fill(tr.contract, ex, cr, ex.time))
        if not self.conectado or ex.execId in self._fills:
            return                       # nobody listening, or repeated
        fill = Fill(tr.contract, ex, CommissionReport(), self.t)
        self._fills[ex.execId] = fill
        tr.fills.append(fill)
        self.execDetailsEvent.emit(tr, fill)
        tr.fillEvent.emit(tr, fill)
    def _commission(self, tr, cr):
        fill = self._fills.get(cr.execId)
        if fill is not None and self.conectado:
            for k, v in vars(cr).items():
                setattr(fill.commissionReport, k, v)
            self.commissionReportEvent.emit(tr, fill,
                                            fill.commissionReport)
    def _cae(self):
        self.conectado = False
        self.disconnectedEvent.emit()
    def reqExecutions(self):
        return list(self.broker_fills)

Contracts: qualify before use

A Contract describes an instrument. The IBKR documentation gives two minimal ways of identifying it unambiguously: the conId (IBKR's unique identifier) with the destination (exchange), or the symbol, type, destination, primary exchange (primaryExchange) and currency; derivatives also need the expiration, the multiplier or the trading class6Note 6 · Interactive Brokers, TWS API, The Contract Object, accessed on October 22, 2026.. qualifyContracts asks the server for the details and fills in the contract in place, conId included. The real code:

IN [05]ib_contratos.pypython
from ib_async import IB, Contract, Future, Stock

ib = IB()
ib.connect("127.0.0.1", 4002, clientId=7)
pedidos = [Stock("ORSC", "SMART"),                 # which currency?
           Stock("ORSC", "SMART", "USD"),
           Future("QES", "202603", "CME")]         # month YYYYMM
calificados = ib.qualifyContracts(*pedidos)
for pedido, c in zip(pedidos, calificados):
    if c is None:                                  # 2.1.0: None
        print("not qualified:", pedido)
    else:
        print(c.conId, c.localSymbol, c.primaryExchange)

In version 2.1.0 the result keeps one position per requested contract and puts None in those that cannot be qualified, whether because they do not exist or because they are ambiguous (qualifyContractsAsync(..., returnAll=True) returns the list of candidates in that slot); before, the ambiguity was only noted in the log. The simulator has a fictitious catalog with the ORSC ETF from the module 4.5 data, a namesake in Canadian dollars and two expirations of a made-up future:

IN [06]python
catalogo = [
    Contract("STK", conId=700101, symbol="ORSC", exchange="SMART",
             primaryExchange="ARCA", currency="USD",
             localSymbol="ORSC"),
    Contract("STK", conId=700102, symbol="ORSC", exchange="SMART",
             primaryExchange="TSE", currency="CAD",
             localSymbol="ORSC"),
    Contract("FUT", conId=700201, symbol="QES",
             lastTradeDateOrContractMonth="20260320", multiplier="50",
             exchange="CME", currency="USD", localSymbol="QESH6"),
    Contract("FUT", conId=700202, symbol="QES",
             lastTradeDateOrContractMonth="20260618", multiplier="50",
             exchange="CME", currency="USD", localSymbol="QESM6"),
]
T0 = datetime(2025, 10, 14, 13, 35, 0, 805771, tzinfo=timezone.utc)
ib = IBSimulado(catalogo, T0)
errores = []
ib.errorEvent += lambda rid, cod, txt, c: errores.append((cod, txt))
ib.connect("127.0.0.1", 4002, clientId=7)

pedidos = [Stock("ORSC", "SMART"), Stock("ORSC", "SMART", "USD"),
           Contract("FUT", symbol="QES",
                    lastTradeDateOrContractMonth="202603"),
           Stock("ORSQ", "SMART", "USD")]
for c in ib.qualifyContracts(*pedidos):
    print(None if c is None else
          (c.conId, c.localSymbol, c.primaryExchange or c.exchange,
           c.currency, c.multiplier))
print(errores)
OUT [06]Qualifying contracts: ambiguity, currency and expiration
None
(700101, 'ORSC', 'ARCA', 'USD', '')
(700201, 'QESH6', 'CME', 'USD', '50')
None
[(200, 'No security definition has been found for the request')]

ORSC without a currency is ambiguous (two stocks with the same symbol) and comes back as None with no error; with the currency it is qualified with its conId and primary exchange, keeping SMART as the destination (the IBKR router). The future requested by month, 202603, resolves to the March contract, with its local symbol and multiplier, which you have to use to compute the notional (lesson 3.1.1). The symbol that does not exist comes back as None and, in addition, with an error 200 (“No security definition has been found”)7Note 7 · Interactive Brokers, TWS API, Error Codes (200, 201, 202, 326, 354), accessed on October 22, 2026.. Two habits that avoid trouble: store the conId of each instrument in the system's configuration (it is stable and unambiguous) and check for None before sending anything.

Market data: knowing what you are looking at

reqMktData returns an empty Ticker that fills up as ticks arrive. Without a subscription to the data of the corresponding market (lesson 1.3.6), IBKR does not send real-time data over the API; you can request another data type with reqMarketDataType8Note 8 · Interactive Brokers, TWS API, Delayed Market Data (data types 1 to 4), accessed on October 22, 2026.:

TypeNameWhat it is, according to IBKR
1LiveReal-time data; requires a subscription
2FrozenThe last quote recorded at the close; same subscription as type 1
3DelayedFree data with a 15-20 minute delay
4Delayed frozenThe delayed data from the close, for those without a subscription

The Ticker stores the type in marketDataType. That field is the defense against an expensive mistake: a system that loses its subscription (or that starts on a new machine with the default configuration) can keep receiving delayed prices and trade on them without noticing.

IN [07]ib_datos.pypython
ib.reqMarketDataType(3)                       # delayed if no type 1
ticker = ib.reqMktData(contrato, "", False, False)
ib.sleep(2)                                   # it fills up gradually
print(ticker.marketDataType, ticker.bid, ticker.ask,
      ticker.midpoint())
ib.cancelMktData(contrato)
IN [08]python
cot = {700201: [(200, 5401.25, 5401.50), (900, 5401.50, 5401.75)]}
ib = IBSimulado(catalogo, T0, suscritos={700101}, cotizaciones=cot)
errores = []
ib.errorEvent += lambda rid, cod, txt, c: errores.append((cod, txt))
ib.connect(clientId=7)
qes = ib.qualifyContracts(Contract(
    "FUT", symbol="QES", lastTradeDateOrContractMonth="202603"))[0]

t1 = ib.reqMktData(qes)              # type 1 without a subscription
ib.sleep(1)
print("type 1:", errores, t1.midpoint())
ib.reqMarketDataType(3)
t3 = ib.reqMktData(qes)              # delayed
ib.sleep(1)
print("type 3:", t3.marketDataType, t3.bid, t3.ask, t3.midpoint())

def puedo_operar(tk):
    """Only with real-time data and both sides of the quote."""
    return tk.marketDataType == 1 and tk.hasBidAsk()
print("trade on these prices?", puedo_operar(t3))
OUT [08]Live data, delayed data and a safety rule
type 1: [(354, 'Requested market data is not subscribed')] nan
type 3: 3 5401.5 5401.75 5401.625
trade on these prices? False

With type 1 and no subscription, error 354 (“Requested market data is not subscribed”) arrives and the ticker stays empty (nan); with type 3, bid and ask arrive, but delayed. The puedo_operar rule rejects them. In a real system the rule goes in the risk manager (lesson 4.5.1) and comes with another: a maximum age for the last tick.

Orders, statuses and events

placeOrder(contrato, orden) returns a Trade in PendingSubmit status and the rest arrives through messages. The statuses IBKR documents9Note 9 · Interactive Brokers, TWS API, Understanding Order Status Message, accessed on October 22, 2026.:

PendingSubmitPreSubmittedSubmittedFilledPendingCancelCancelledInactive
Mermaid source
stateDiagram-v2
  [*] --> PendingSubmit
  PendingSubmit --> PreSubmitted
  PendingSubmit --> Submitted
  PreSubmitted --> Submitted
  Submitted --> Filled
  Submitted --> PendingCancel
  PendingCancel --> Cancelled
  Submitted --> Cancelled
  PendingSubmit --> Inactive

PreSubmitted is an order that has been accepted but not yet activated (an order simulated by IBKR, or the child of a bracket until the parent executes); Submitted, an order active at its destination; PendingCancel, a cancellation requested and not confirmed, during which an execution can still arrive. A cancellation can also arrive without the program asking for it, when the destination rejects the order: codes 201 (“Order rejected”) and 202 (“Order cancelled”) explain the reason7. ib_async considers the Filled, Cancelled, ApiCancelled and Inactive statuses final (trade.isDone()).

Each execution arrives as execDetails with an Execution object (execId, time, side BOT or SLD, shares, price, cumQty, avgPrice, orderRef…), and its commission afterwards, in a commissionReport with the same execId10Note 10 · Interactive Brokers, TWS API, The Execution Object, ExecID Behavior and Commission And Fees Report, accessed on October 22, 2026.. The current TWS API calls that object CommissionAndFeesReport; ib_async 2.1.0 keeps the name CommissionReport and the commissionReportEvent event. The real code of a system that subscribes to the events:

IN [09]ib_orden_eventos.pypython
from ib_async import IB, MarketOrder, Stock

ib = IB()
ib.connect("127.0.0.1", 4002, clientId=7)
orsc = ib.qualifyContracts(Stock("ORSC", "SMART", "USD"))[0]

def al_estado(trade):
    s = trade.orderStatus
    print(trade.order.orderRef, s.status, s.filled, s.remaining)

def al_ejecutar(trade, fill):
    e = fill.execution
    print(e.execId, e.side, e.shares, e.price)

def a_la_comision(trade, fill, informe):
    print(informe.execId, informe.commission, informe.currency)

ib.orderStatusEvent += al_estado
ib.execDetailsEvent += al_ejecutar
ib.commissionReportEvent += a_la_comision
trade = ib.placeOrder(orsc, MarketOrder("BUY", 434, orderRef="R00005"))
trade.filledEvent += lambda t: print("filled",
                                    t.orderStatus.avgFillPrice)
ib.run()                                       # let the loop run

The simulator reproduces order R00005 from the live account of module 4.5: a market buy of 434 ORSC with three executions, with the messages from brokers4_ejemplos.json. Before using that file you have to check its fields against the documentation: the names in orderStatus, execution and commissionReport match the ib_async 2.1.0 classes, but its execIds have three segments (0000e0d5.710000035.01) where IBKR documents four, with the last one reserved for corrections (….01 and its correction ….02)10. Read literally, the second execId in the file would be a correction of the first. The script adds the fourth segment. The execution times are those in the file; the submission (30 ms) is the difference between created_at and submitted_at of the same order in the Alpaca message, and the 2 ms between the execution and its commission are an assumption of the simulator.

IN [10]python
import json

ej = json.load(open("brokers4_ejemplos.json"))["interactive_brokers"]

def guion_r00005(ib, tr, ej, t_envio, retraso_cr_ms=2):
    """Schedules the messages of order R00005 in the simulator."""
    def ms(ts):
        return (datetime.fromisoformat(ts) - t_envio) / \
            timedelta(milliseconds=1)
    st = ej["orderStatus"]          # st[0], Submitted: placeOrder
    for f, s in zip(ej["fills"], st[1:]):
        e = f["execution"]
        ex = Execution(**{k: e[k] for k in
                          Execution.__dataclass_fields__ if k in e})
        ex.time = datetime.fromisoformat(e["time"])
        ex.execId = e["execId"] + ".01"      # 4 segments (IBKR)
        ex.orderId = tr.order.orderId
        cr = CommissionReport(ex.execId,
                              f["commissionReport"]["commission"],
                              "USD")
        dt = ms(e["time"])
        ib.programa(dt, ib._exec, tr, ex, cr)
        ib.programa(dt, ib._status, tr, {k: s[k] for k in (
            "status", "filled", "remaining", "avgFillPrice",
            "lastFillPrice")})
        ib.programa(dt + retraso_cr_ms, ib._commission, tr, cr)

ib = IBSimulado(catalogo, T0)
ib.connect("127.0.0.1", 4002, clientId=7)
orsc = ib.qualifyContracts(Stock("ORSC", "SMART", "USD"))[0]
registro = []
ib.orderStatusEvent += lambda tr: registro.append(
    (ib.ms(), "orderStatus", tr.orderStatus.status,
     tr.orderStatus.filled))
ib.execDetailsEvent += lambda tr, f: registro.append(
    (ib.ms(), "execDetails", f.execution.execId[-9:],
     f.execution.shares))
ib.commissionReportEvent += lambda tr, f, r: registro.append(
    (ib.ms(), "commissionReport", r.execId[-9:], r.commission))

tr = ib.placeOrder(orsc, MarketOrder("BUY", 434, orderRef="R00005"))
guion_r00005(ib, tr, ej, T0)
ib.sleep(5)
for ms, evento, a, b in registro:
    print(f"{ms:8.1f} ms  {evento:<16s} {a:<10} {b}")
print("done:", tr.isDone(), "average price:",
      tr.orderStatus.avgFillPrice, "commission:",
      round(sum(f.commissionReport.commission for f in tr.fills), 2))
OUT [10]Order R00005, message by message
    30.0 ms  orderStatus      Submitted  0.0
   219.2 ms  execDetails      035.01.01  27.0
   219.2 ms  orderStatus      Submitted  27.0
   221.2 ms  commissionReport 035.01.01  0.35
  1716.3 ms  execDetails      035.02.01  389.0
  1716.3 ms  orderStatus      Submitted  416.0
  1718.3 ms  commissionReport 035.02.01  1.36
  3635.6 ms  execDetails      035.03.01  18.0
  3635.6 ms  orderStatus      Filled     434.0
  3637.6 ms  commissionReport 035.03.01  0.35
done: True average price: 234.8353 commission: 2.06

The figure at the top draws that sequence. The order is accepted at 30 ms; the first execution (27 shares) arrives at 219 ms; the second, the large one (389), at 1.7 s; and the last one (18) at 3.6 s, with the Filled status. The average price, 234.8353, and the total commission, 2.06 dollars, are those in the file. Three details a system has to respect:

  • The order of the messages. The execution arrives before the status that reflects it, and the commission after it. A handler that computes the cost of the order in orderStatusEvent does not have the commission yet.
  • The status is not enough. Submitted with filled = 416 is a partially executed order: the position has already changed. Positions are tracked with executions (execDetails), not with statuses.
  • Your own identifier. orderRef travels with the order and comes back in each Execution: it is what lets you recognize your own orders after a reconnection. The API reference describes it as “intended for institutional customers only”, but the reference for the Execution object presents it as a string configurable from the API that accompanies the order throughout its life10; it is worth checking on the paper account that it is preserved.

Bracket: an order with its stop or none at all

An entry order with its take-profit and its stop (lesson 1.1.4) is sent at IBKR as a bracket: the parent and two children with parentId. IB.bracketOrder in ib_async creates them with transmit=False on the parent and the take-profit and transmit=True on the stop. The API reference defines the field: if it is false, the order is created in TWS but not sent11Note 11 · Interactive Brokers, TWS API, reference for the Order object (transmit, parentId, ocaGroup, orderRef), accessed on October 22, 2026.. That way the group goes out whole when the last one arrives, and if the program crashes halfway, nothing goes out.

IN [11]ib_bracket.pypython
bracket = ib.bracketOrder("BUY", 100, limitPrice=234.80,
                          takeProfitPrice=240.00,
                          stopLossPrice=230.00)
for o in bracket:                       # parent, take-profit, stop
    o.orderRef = "R00999"
    ib.placeOrder(orsc, o)

The simulator holds back orders with transmit=False and sends the group with the one that arrives with transmit=True. The cell compares the correct bracket with the mistake of sending the parent with transmit=True and crashing before the children:

IN [12]python
def bracket(ib, accion, q, lmt, beneficio, stop):
    """Like IB.bracketOrder in ib_async (consecutive ids)."""
    contraria = "SELL" if accion == "BUY" else "BUY"
    i = ib.next_id
    ib.next_id += 3
    return (LimitOrder(accion, q, lmt, orderId=i, transmit=False),
            LimitOrder(contraria, q, beneficio, orderId=i + 1,
                       transmit=False, parentId=i),
            StopOrder(contraria, q, stop, orderId=i + 2,
                      transmit=True, parentId=i))

def estados(ib):
    return {t.order.orderId: t.orderStatus.status
            for t in ib.trades.values()}

ib = IBSimulado(catalogo, T0)
ib.connect(clientId=7)
padre, benef, stop = bracket(ib, "BUY", 100, 234.80, 240.0, 230.0)
ib.placeOrder(orsc, padre)
ib.placeOrder(orsc, benef)
ib.sleep(1)
print("before the stop:", estados(ib))
ib.placeOrder(orsc, stop)
ib.sleep(1)
print("with the stop:  ", estados(ib))

ib2 = IBSimulado(catalogo, T0)
ib2.connect(clientId=8)
padre, benef, stop = bracket(ib2, "BUY", 100, 234.80, 240.0, 230.0)
padre.transmit = True                  # the mistake
ib2.placeOrder(orsc, padre)            # ...and the program crashes
ib2.sleep(1)
print("parent alone:   ", estados(ib2))
OUT [12]The transmit field in a bracket
before the stop: {1: 'PendingSubmit', 2: 'PendingSubmit'}
with the stop:   {1: 'Submitted', 2: 'PreSubmitted', 3: 'PreSubmitted'}
parent alone:    {1: 'Submitted'}

With the correct bracket, the parent and the take-profit wait in PendingSubmit (created, not sent) until the stop arrives; then the group goes out: the parent active (Submitted) and the children in PreSubmitted, waiting for the parent to execute. In the second case the parent goes out alone and stays active: if it executes, the position has no stop. The same reasoning applies to any group of orders that must go out together (OCA groups, one cancels all, which ib_async builds with IB.oneCancelsAll).

Reconnection and reconciliation

A disconnection in the middle of an order is the norm, not the exception: daily restart, maintenance, network. While the program is not listening, the broker keeps executing. On reconnecting you have to request the executions (reqExecutions, which also accepts an ExecutionFilter) and the open orders (reqAllOpenOrders) and merge them with what the program already had, with two rules from the IBKR documentation: each partial execution has its own execId, and a correction arrives as another execution with the same data except the execId, which differs only in the digits after the last period10. ib_async, in addition, clears all its internal state on disconnecting and requests it again on connecting (positions, open and completed orders, executions), so it is the system's own book that has to be reconciled.

IN [13]ib_reconexion.pypython
from ib_async import ExecutionFilter

def al_error(req_id, codigo, texto, contrato):
    if codigo == 1100:
        print("no connection to the servers")
    elif codigo == 1101:                 # data lost
        resuscribir_datos()
    elif codigo == 1102:                 # data maintained
        reconciliar()

ib.errorEvent += al_error
ib.disconnectedEvent += lambda: programar_reconexion()

def reconciliar():
    fills = ib.reqExecutions(ExecutionFilter(clientId=7))
    abiertas = ib.reqAllOpenOrders()
    fusionar(fills, abiertas)            # by execId and orderRef

In the simulator, the API disconnects one second after sending R00005 (a Gateway restart), the second execution arrives while nobody is listening and, once reconnected, the broker publishes a correction of the price of the third:

IN [14]python
ib = IBSimulado(catalogo, T0)
ib.connect(clientId=7)
libro = {}                                   # execId -> Execution
ib.execDetailsEvent += lambda tr, f: libro.setdefault(
    f.execution.execId, f.execution)
tr = ib.placeOrder(orsc, MarketOrder("BUY", 434, orderRef="R00005"))
guion_r00005(ib, tr, ej, T0)
ib.programa(1000, ib._cae)                   # Gateway goes down
ib.programa(2500, ib.connect, "127.0.0.1", 4002, 7)
ib.sleep(4)
# 50 s later, the broker corrects the price of the third one
tercera = ib.broker_fills[-1].execution
corr = Execution(**{**vars(tercera), "price": 234.83,
                    "execId": tercera.execId[:-2] + "02"})
ib.programa(50_000, ib._exec, tr, corr, CommissionReport(
    corr.execId, 0.0, "USD"))
ib.sleep(60)

def posicion(ejecuciones):
    """One execution per execId base: the latest correction wins."""
    vigentes = {}
    for e in ejecuciones:
        base, corr = e.execId.rsplit(".", 1)
        if base not in vigentes or corr > vigentes[base].execId[-2:]:
            vigentes[base] = e
    q = sum(e.shares for e in vigentes.values())
    px = sum(e.shares * e.price for e in vigentes.values()) / q
    return q, round(px, 4), len(vigentes)

print("only what was heard:", posicion(libro.values()))
recibidas = [f.execution for f in ib.reqExecutions()]
print("reqExecutions:      ", len(recibidas), "messages")
sumadas = sum(e.shares for e in recibidas)
print("adding everything:  ", sumadas, "shares")
print("merged and with the correction:", posicion(recibidas))
OUT [14]Disconnection, reqExecutions and a correction
only what was heard: (45.0, 234.716, 2)
reqExecutions:       4 messages
adding everything:   452.0 shares
merged and with the correction: (434.0, 234.8361, 3)

What the program heard gives it 45 shares (the first and third executions): the large one was lost during the disconnection. reqExecutions returns four messages, the three executions and the correction; simply adding them up gives 452 shares, 18 too many. Merged by execId, with the correction replacing the third, the position is the true one: 434 shares at an average price of 234.8361 (the correction raised the price of 18 shares by two cents). This is the same problem that lessons 4.5.2 and 4.5.5 solve in general with the logs of the live system (duplicated, lost and corrected executions); here with IBKR's specific rules.

Pacing limits

Lesson 1.3.6 explained IBKR's limits for historical data. For all requests, the documentation sets a maximum number of requests per second equal to the market data lines divided by two: with the default 100 lines, 50 per second; above that, some orders may be queued and delayed12Note 12 · Interactive Brokers, TWS API, Pacing Limitations; ib_async 2.1.0, Client class (source code), accessed on October 22, 2026.. ib_async throttles on its own: its client limits itself to 45 requests per second (MaxRequests = 45, RequestsInterval = 1), below the limit. What that means for a burst of orders:

IN [15]python
def salidas(n, maximo=45, ventana=1.0):
    """Send time of n requests launched at once at t=0."""
    t = []
    for i in range(n):
        ahora = t[-1] if t else 0.0
        if len(t) >= maximo:
            ahora = max(ahora, t[-maximo] + ventana)
        t.append(ahora)
    return t

s = salidas(120)
por_segundo = {seg: sum(int(x) == seg for x in s) for seg in range(3)}
print("sent per second:", por_segundo)
print(f"the last one goes out at {s[-1]:.1f} s")
OUT [15]A burst of 120 requests with the ib_async throttle
sent per second: {0: 45, 1: 45, 2: 30}
the last one goes out at 2.0 s
Fig. — Burst of 120 requests launched at once: cumulative requests sent with ib_async's throttle (45 per second) against IBKR's maximum with 100 market data lines (50 per second)

The 120 requests take two seconds to go out: 45, 45 and 30. For the module 4.5 system, which rebalances ten ETFs once a day, it is irrelevant. For one that reacts to many instruments at once (or that cancels and resends orders in a loop), two seconds of queue change the prices at which it executes, and the sending order matters: risk-reducing orders first.

Typical mistakes

  • Using time.sleep. It freezes the ib_async loop: no statuses or executions arrive while it sleeps.
  • Not checking for None after qualifyContracts. In version 2.1.0 an ambiguous contract does not raise an exception.
  • Trading on delayed data. You have to check ticker.marketDataType and the age of the last tick.
  • Tracking the position with statuses. The position comes from the executions, merged by execId.
  • Sending the parent of a bracket with transmit=True. If the program crashes, a position is left without a stop.
  • Using the same clientId in two programs. IBKR rejects the second connection (error 326); in addition, each connection only sees its own orders, except the one with the master client ID configured in TWS13Note 13 · Interactive Brokers, TWS API, ClientId 0 and the Master Client ID, accessed on October 22, 2026..
  • Forgetting the daily restart. A system that does not reconnect by itself stops trading once a day.

Exercises

  1. Modify the R00005 script so that the broker rejects the order (Cancelled status with error 201 at 20 ms, no executions) and check what tr.isDone() returns and what a handler connected to errorEvent receives.
Solution
IN [16]python
ib = IBSimulado(catalogo, T0)
ib.connect(clientId=7)
avisos = []
ib.errorEvent += lambda rid, cod, txt, c: avisos.append((rid, cod))
tr = ib.placeOrder(orsc, MarketOrder("BUY", 434, orderRef="R00005"))
ib.programa(20, ib.errorEvent.emit, tr.order.orderId, 201,
            "Order rejected - Reason: (simulated)", orsc)
ib.programa(20, ib._status, tr, {"status": "Cancelled"})
ib.sleep(1)
print(tr.isDone(), tr.orderStatus.status, tr.log, avisos)
OUT [16]A rejected order
True Cancelled [(0.0, 'PendingSubmit'), (20.0, 'Cancelled')] [(1, 201)]

The order finishes (isDone() is true) in Cancelled, with no executions, and the error arrives with the orderId as the request identifier: that is how the reason is linked to the order. A system must not blindly retry a rejection; the error text (size, price out of range, margin) says whether it makes sense.

  1. With the posicion function, what average price and how many executions would you accept if the correction of the third execution arrived before the original execution (out of order)?
Solution
IN [17]python
desordenadas = [recibidas[0], recibidas[1], recibidas[3],
                recibidas[2]]
print(posicion(desordenadas))
OUT [17]The correction arrives first
(434.0, 234.8361, 3)

The result does not change: for each execId base, the function keeps the highest correction suffix, not the last one to arrive. A version that kept the last message received would give the uncorrected price. The identifiers, not the order of arrival, decide.

  1. A system connected to IB Gateway on a paper account receives message 1101 at 6:00 in the morning and, at 9:30, no new quotes. What has happened and what should it have done?
Solution

1101 means that the connection between Gateway and the servers was restored but the data subscriptions were lost and have to be requested again. If the program did not request them, its Tickers kept the last values from before the outage and are not updating. It should have called reqMktData again for each contract on receiving 1101 (with 1102 it is not necessary) and monitored the age of the last tick: a price that does not change for hours while the market is open is an alarm, not a quiet market.

Footnotes

  1. Interactive Brokers, TWS API, The IB Gateway, accessed on October 22, 2026. ↩ ↩2

  2. Interactive Brokers, TWS API, Daily & Weekly Reauthentication, accessed on October 22, 2026. ↩

  3. Interactive Brokers, TWS API, System Message Codes (1100, 1101, 1102 and 1300), accessed on October 22, 2026. ↩ ↩2

  4. IBC, repository README (“This project has been retired, as of 1 September 2026”); ib_async 2.1.0, Watchdog class (source code), accessed on October 22, 2026. ↩

  5. ib_async on PyPI (version 2.1.0, published on December 8, 2025; dependencies aeventkit>=2.1.0,<3.0.0, nest_asyncio and tzdata>=2025.2,<2026.0) and API documentation, accessed on October 22, 2026. ↩

  6. Interactive Brokers, TWS API, The Contract Object, accessed on October 22, 2026. ↩

  7. Interactive Brokers, TWS API, Error Codes (200, 201, 202, 326, 354), accessed on October 22, 2026. ↩ ↩2

  8. Interactive Brokers, TWS API, Delayed Market Data (data types 1 to 4), accessed on October 22, 2026. ↩

  9. Interactive Brokers, TWS API, Understanding Order Status Message, accessed on October 22, 2026. ↩

  10. Interactive Brokers, TWS API, The Execution Object, ExecID Behavior and Commission And Fees Report, accessed on October 22, 2026. ↩ ↩2 ↩3 ↩4

  11. Interactive Brokers, TWS API, reference for the Order object (transmit, parentId, ocaGroup, orderRef), accessed on October 22, 2026. ↩

  12. Interactive Brokers, TWS API, Pacing Limitations; ib_async 2.1.0, Client class (source code), accessed on October 22, 2026. ↩

  13. Interactive Brokers, TWS API, ClientId 0 and the Master Client ID, accessed on October 22, 2026. ↩

Each answer is folded under its question

Self-assessment

  1. According to the TWS API documentation, what is the difference between TWS and IB Gateway for a program that uses the API?

    • AIB Gateway has a more complete API
    • BFor the API they are identical: both are the server the socket is opened to after authenticating; Gateway lacks the graphical tools and uses about 40% fewer resources
    • CTWS does not accept API connections
    • DIB Gateway needs no username or password
    Show answer

    Answer B. For the API they are identical: both are the server the socket is opened to after authenticating; Gateway lacks the graphical tools and uses about 40% fewer resources

    The documentation says that, from the API's point of view, IB Gateway and TWS are identical, that in both you have to enter a username and password in a login window and that Gateway is lighter (around 40% fewer resources).

  2. After ib.qualifyContracts(Stock('ORSC', 'SMART')), the returned list contains None. What has happened in ib_async 2.1.0?

    • AThe connection failed
    • BThe contract is qualified and None is its conId
    • CThe contract is unknown or ambiguous (for example, the same symbol in two currencies); you need to add fields (currency, primaryExchange) or use the conId
    • DYou need to wait longer
    Show answer

    Answer C. The contract is unknown or ambiguous (for example, the same symbol in two currencies); you need to add fields (currency, primaryExchange) or use the conId

    In version 2.1.0, the result keeps the position of each requested contract and puts None in those that cannot be qualified. Before, it was only logged as a warning. A system that does not check for that None sends orders with an incomplete contract.

  3. A system with no data subscription for a future calls ib.reqMarketDataType(3) and receives a Ticker with bid and ask. What must it check before trading on those prices?

    • Aticker.marketDataType: 3 means delayed data (15-20 minutes according to the documentation), which is no good for deciding an order
    • BNothing: if there are bid and ask, they are good
    • CThat the bid is lower than the ask
    • DThe number of market data lines
    Show answer

    Answer A. ticker.marketDataType: 3 means delayed data (15-20 minutes according to the documentation), which is no good for deciding an order

    With type 3, IBKR sends delayed data with its own tick types. The ib_async Ticker stores the type in marketDataType. A simple safety rule is not to generate orders if the type is not 1 (real-time data).

  4. In an ib_async bracket order, the parent order and the take-profit go with transmit=False and the stop with transmit=True. Why?

    • AFor compatibility with old versions
    • BSo that the stop order executes first
    • CBecause the stop does not accept transmit=False
    • DBecause an order with transmit=False is created in TWS without being sent; when the last one arrives with transmit=True the whole group is sent, so the parent is never left active without its children
    Show answer

    Answer D. Because an order with transmit=False is created in TWS without being sent; when the last one arrives with transmit=True the whole group is sent, so the parent is never left active without its children

    The API reference defines transmit: if it is False, the order is created in TWS but not sent. The ib_async bracket uses that so the group goes out together. If the program crashes between the parent and the children, nothing goes out; if the parent went with transmit=True, it could execute without a stop.

  5. After a reconnection, reqExecutions() returns one execution with execId …035.03.02 and another with …035.03.01, identical except for the price. What does the IBKR documentation say?

    • AThey are two different executions: add both
    • BThe .02 is a correction of the .01: the execId only differs in the digits after the last period, and it replaces the earlier one
    • CIt is a broker error to be ignored
    • DThe .01 is the right one because it arrived first
    Show answer

    Answer B. The .02 is a correction of the .01: the execId only differs in the digits after the last period, and it replaces the earlier one

    The ExecID documentation says that a correction arrives as another execDetails with the same parameters except the execId, which differs only in the digits after the last period. Counting them as two doubles the position; keeping the first leaves a price the broker has already corrected.

Key points

  • TWS and IB Gateway are, for the API, the same local server; Gateway is lighter. Both restart every day (with automatic restart since version 974) and ask for credentials again every week; a system has to plan for those disconnections.
  • ib_async 2.1.0 (latest version on PyPI, December 8, 2025) wraps the TWS API in asyncio: synchronous calls, Trade and Ticker objects that update themselves and events. You wait with ib.sleep or ib.run, never with time.sleep.
  • Qualifying contracts fills in the conId; in 2.1.0 an ambiguous or unknown contract comes back as None in its position, and you have to check for it before using it.
  • The market data type (1 live, 2 frozen, 3 delayed, 4 delayed frozen) travels in the Ticker: a system must not trade on data that is not type 1.
  • Order statuses (PendingSubmit, PreSubmitted, Submitted, Filled, Cancelled, Inactive) arrive through orderStatus; executions through execDetails and their commission through commissionReport, afterwards. The execId identifies each execution and its corrections.
  • After a disconnection, state is rebuilt by requesting executions and orders from the broker and merging them by execId with what you already had: without duplicates and applying corrections.

Resources

Data