---
title: "4.4.2 Interactive Brokers with ib_async"
description: "TWS and IB Gateway, restarts and reauthentication, ib_async 2.1.0: contracts, market data, bracket orders, events and reconnection, with a simulated IB."
url: "https://pythonparatrading.com/en/python-trading-course/level-4/interactive-brokers-with-ib-async/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-4/interactive-brokers-with-ib-async.png"
---

# 4.4.2 · 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.

- Course: [Level 4: Portfolios, risk, execution and live trading](https://pythonparatrading.com/en/python-trading-course/level-4/)
- Module: 4.4 · Brokers and APIs
- Duration: 70 min
- Prerequisites: [1.1.4 · Order types](https://pythonparatrading.com/en/python-trading-course/level-1/order-types-explained/), [1.3.6 · Brokers as market data sources](https://pythonparatrading.com/en/python-trading-course/level-1/brokers-as-market-data-sources/), [3.3.3 · Currency risk in multi-currency portfolios; execution via OANDA, IBKR or MT5](https://pythonparatrading.com/en/python-trading-course/level-3/currency-risk-and-fx-execution/), [4.4.1 · Choosing a broker from Spain or the EU: regulation, assets, costs and API](https://pythonparatrading.com/en/python-trading-course/level-4/choosing-a-broker-in-the-eu/)

## By the end of this lesson

- Choose between TWS and IB Gateway for a system and plan for their daily restarts, weekly reauthentication and login automation.
- Use the ib_async 2.1.0 model: a synchronous API on top of asyncio, objects that update themselves (Trade, Ticker) and events to which functions subscribe.
- Qualify contracts (conId, primaryExchange, expirations) and handle the None result of an ambiguous or unknown contract.
- Request market data knowing its type (live, frozen, delayed) and never trade on delayed data by mistake.
- Send simple and bracket orders respecting the transmit field, follow their statuses and executions through events and reconcile after a disconnection with reqExecutions, without duplicating executions and applying corrections.

## 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 tools[^1].

| | TWS | IB Gateway |
|---|---|---|
| Default ports (live / paper) | 7496 / 7497 | 4001 / 4002 |
| Interface | Full platform | Minimal window |
| Resources | More | Around 40% fewer |
| Typical use | Development, manual monitoring | Server 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
  again[^1]. 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 due[^2].
- **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 recommended[^3].
- **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
  support[^4]. Whoever uses it inherits that maintenance risk.

```mermaid
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 did[^3]. 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`)[^5]. **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.

```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)
```
```output
Python 3.14.9
ib_async installed: False
```

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

```python skip title="ib_conexion.py"
# 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`.

```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])
```
```output
['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.

```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
class[^6]. `qualifyContracts` asks the server for the details and fills in the contract **in place**, conId included.
The real code:

```python skip title="ib_contratos.py"
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:

```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)
```
```output
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”)[^7]. 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 `reqMarketDataType`[^8]:

| Type | Name | What it is, according to IBKR |
|---|---|---|
| 1 | Live | Real-time data; requires a subscription |
| 2 | Frozen | The last quote recorded at the close; same subscription as type 1 |
| 3 | Delayed | Free data with a 15-20 minute delay |
| 4 | Delayed frozen | The 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.

```python skip title="ib_datos.py"
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)
```

```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))
```
```output
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 documents[^9]:

```mermaid
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 reason[^7]. `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
`execId`[^10]. 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:

```python skip title="ib_orden_eventos.py"
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 `execId`s 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.

```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))
```
```output
    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 life[^10]; 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 sent[^11]. That way the group goes out whole when the last one arrives, and if the program crashes
halfway, nothing goes out.

```python skip title="ib_bracket.py"
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:

```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))
```
```output
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 period[^10]. `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.

```python skip title="ib_reconexion.py"
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:

```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))
```
```output
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 delayed[^12]. `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:

```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")
```
```output
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).* Two rising staircases from 0 to 120 requests in 2 seconds. The ib_async one, highlighted, steps to 45 at t = 0, 90 at t = 1 s and 120 at t = 2 s; the IBKR limit one, dashed, steps to 50, 100 and 120 at the same instants.

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 TWS[^13].
- **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.**

```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)
```
```output
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.

2. 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.**

```python
desordenadas = [recibidas[0], recibidas[1], recibidas[3],
                recibidas[2]]
print(posicion(desordenadas))
```
```output
(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.

3. 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 `Ticker`s 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.

[^1]: Interactive Brokers, TWS API, [The IB Gateway](https://www.interactivebrokers.com/docs/tws-api/doc/architecture/the-trader-workstation/the-ib-gateway),
    accessed on October 22, 2026.
[^2]: Interactive Brokers, TWS API, [Daily & Weekly Reauthentication](https://www.interactivebrokers.com/docs/tws-api/doc/tws-settings/daily-weekly-reauthentication),
    accessed on October 22, 2026.
[^3]: Interactive Brokers, TWS API, [System Message Codes](https://www.interactivebrokers.com/docs/tws-api/doc/error-handling/system-message-codes)
    (1100, 1101, 1102 and 1300), accessed on October 22, 2026.
[^4]: IBC, [repository README](https://github.com/IbcAlpha/IBC) (“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](https://pypi.org/project/ib_async/) (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](https://ib-api-reloaded.github.io/ib_async/api.html), accessed on October 22, 2026.
[^6]: Interactive Brokers, TWS API, [The Contract Object](https://www.interactivebrokers.com/docs/tws-api/doc/contracts-financial-instruments/the-contract-object),
    accessed on October 22, 2026.
[^7]: Interactive Brokers, TWS API, [Error Codes](https://www.interactivebrokers.com/docs/tws-api/doc/error-handling/error-codes)
    (200, 201, 202, 326, 354), accessed on October 22, 2026.
[^8]: Interactive Brokers, TWS API, [Delayed Market Data](https://www.interactivebrokers.com/docs/tws-api/doc/md/delayed-data)
    (data types 1 to 4), accessed on October 22, 2026.
[^9]: Interactive Brokers, TWS API, [Understanding Order Status Message](https://www.interactivebrokers.com/docs/tws-api/doc/order-management/order-status/understanding-order-status-message),
    accessed on October 22, 2026.
[^10]: Interactive Brokers, TWS API, [The Execution Object](https://www.interactivebrokers.com/docs/tws-api/doc/order-management/execution-details/the-execution-object),
    [ExecID Behavior](https://www.interactivebrokers.com/docs/tws-api/doc/order-management/execution-details/exec-id-behavior)
    and [Commission And Fees Report](https://www.interactivebrokers.com/docs/tws-api/doc/order-management/commission-and-fees-report),
    accessed on October 22, 2026.
[^11]: Interactive Brokers, TWS API, reference for the [Order](https://www.interactivebrokers.com/docs/tws-api/ref/order)
    object (`transmit`, `parentId`, `ocaGroup`, `orderRef`), accessed on October 22, 2026.
[^12]: Interactive Brokers, TWS API, [Pacing Limitations](https://www.interactivebrokers.com/docs/tws-api/doc/pacing-limitations/introduction);
    `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](https://www.interactivebrokers.com/docs/tws-api/doc/order-management/client-id-0-and-the-master-client-id),
    accessed on October 22, 2026.

## 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?

   - A. IB Gateway has a more complete API
   - 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
   - C. TWS does not accept API connections
   - D. IB Gateway needs no username or password

   *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?

   - A. The connection failed
   - B. The contract is qualified and None is its conId
   - 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
   - D. You need to wait longer

   *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?

   - A. `ticker.marketDataType`: 3 means delayed data (15-20 minutes according to the documentation), which is no good for deciding an order
   - B. Nothing: if there are bid and ask, they are good
   - C. That the bid is lower than the ask
   - D. The number of market data lines

   *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?

   - A. For compatibility with old versions
   - B. So that the stop order executes first
   - C. Because the stop does not accept transmit=False
   - 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

   *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?

   - A. They are two different executions: add both
   - 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
   - C. It is a broker error to be ignored
   - D. The .01 is the right one because it arrived first

   *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

### Documentation

- [ib_async 2.1.0: API documentation (IB, Trade, Ticker, events)](https://ib-api-reloaded.github.io/ib_async/api.html) · ib-api-reloaded.github.io
- [ib_async (GitHub, ib-api-reloaded)](https://github.com/ib-api-reloaded/ib_async) · github.com
- [IBKR TWS API: The IB Gateway](https://www.interactivebrokers.com/docs/tws-api/doc/architecture/the-trader-workstation/the-ib-gateway) · interactivebrokers.com
- [IBKR TWS API: System Message Codes (1100, 1101, 1102, 1300)](https://www.interactivebrokers.com/docs/tws-api/doc/error-handling/system-message-codes) · interactivebrokers.com
- [IBKR TWS API: Delayed Market Data (data types 1 to 4)](https://www.interactivebrokers.com/docs/tws-api/doc/md/delayed-data) · interactivebrokers.com
- [IBKR TWS API: ExecID Behavior](https://www.interactivebrokers.com/docs/tws-api/doc/order-management/execution-details/exec-id-behavior) · interactivebrokers.com

### Data

- [Level 4 data: sample broker messages, order R00005 (JSON)](https://pythonparatrading.com/curso/datos/nivel-4/brokers4_ejemplos.json) · Download · brokers4_ejemplos.json

Previous lesson: [4.4.1 · Choosing a broker from Spain or the EU: regulation, assets, costs and API](https://pythonparatrading.com/en/python-trading-course/level-4/choosing-a-broker-in-the-eu/)

Next lesson: [4.4.3 · Alpaca, Tradier, MetaTrader 5 with Python and OANDA v20](https://pythonparatrading.com/en/python-trading-course/level-4/alpaca-tradier-metatrader-5-and-oanda/)
