---
title: "4.4.2 Interactive Brokers con ib_async"
description: "TWS e IB Gateway, reinicios y reautenticación, ib_async 2.1.0: contratos, datos de mercado, órdenes con bracket, eventos y reconexión, con un IB simulado."
url: "https://pythonparatrading.com/curso-python-trading/nivel-4/interactive-brokers-con-ib-async/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-4/interactive-brokers-con-ib-async.png"
---

# 4.4.2 · Interactive Brokers: TWS e IB Gateway, ib_async, contratos, datos y órdenes

> La API de Interactive Brokers no habla con sus servidores sino con un programa local que hay que mantener vivo. Esta lección cubre lo que hace falta para operar con ella desde Python: elegir entre TWS e IB Gateway, convivir con sus reinicios, calificar contratos, pedir datos sabiendo de qué tipo son, enviar órdenes con bracket sin dejar una posición sin stop y reconstruir el estado tras una desconexión. El código real de ib_async va sin ejecutar; un IB simulado con reloj lógico reproduce, mensaje a mensaje, una orden real de los datos del curso.

- Curso: [Nivel 4: Carteras, riesgo, ejecución y trading en real](https://pythonparatrading.com/curso-python-trading/nivel-4/)
- Módulo: 4.4 · Brokers y APIs
- Duración: 70 min
- Requisitos: [1.1.4 · Tipos de órdenes](https://pythonparatrading.com/curso-python-trading/nivel-1/tipos-de-ordenes/), [1.3.6 · Brókeres como fuente de datos](https://pythonparatrading.com/curso-python-trading/nivel-1/brokers-como-fuente-de-datos/), [3.3.3 · Riesgo de divisa en carteras multi-moneda; ejecución vía OANDA, IBKR o MT5](https://pythonparatrading.com/curso-python-trading/nivel-3/riesgo-de-divisa-y-ejecucion-fx/), [4.4.1 · Elegir bróker desde España o la UE: regulación, activos, costes y API](https://pythonparatrading.com/curso-python-trading/nivel-4/elegir-broker-desde-espana-y-la-ue/)

## Al terminar esta lección

- Elegir entre TWS e IB Gateway para un sistema y planificar sus reinicios diarios, la reautenticación semanal y la automatización del inicio de sesión.
- Usar el modelo de ib_async 2.1.0: API síncrona sobre asyncio, objetos que se actualizan solos (Trade, Ticker) y eventos a los que se suscriben funciones.
- Calificar contratos (conId, primaryExchange, vencimientos) y tratar el resultado None de un contrato ambiguo o desconocido.
- Pedir datos de mercado sabiendo su tipo (real, congelado, diferido) y no operar nunca con datos diferidos por error.
- Enviar órdenes simples y con bracket respetando el campo transmit, seguir sus estados y ejecuciones por eventos y reconciliar tras una desconexión con reqExecutions, sin duplicar ejecuciones y aplicando las correcciones.

## Lo que ya se vio y lo que falta

La lección 1.3.6 presentó la arquitectura de la API de Interactive Brokers (IBKR): el programa no habla con los
servidores del bróker sino con **TWS** o **IB Gateway**, que corren en la propia máquina y abren un puerto local (7496 y
7497 en TWS, 4001 y 4002 en Gateway, real y papel), y descargó históricos con `ib_async` respetando los límites de
ritmo. La lección 3.3.3 envió una orden de divisas con `Forex`, `LimitOrder`, `whatIfOrder` y `orderRef`. Esta lección
completa lo que hace falta para operar un sistema: el ciclo de vida de TWS y Gateway, el modelo de programación de
`ib_async`, los contratos, los tipos de datos de mercado, las órdenes con bracket, los eventos y la reconexión.

La lección 4.4.1 dejó una tarea previa: comprobar con qué entidad se firma. Interactive Brokers Ireland Limited figura
en la lista de la CNMV de ESI del EEE en libre prestación (inscrita el 29/12/2020) y en el registro de ESMA como empresa
de servicios de inversión activa, autorizada por el Banco Central de Irlanda (consultas del 22/10/2026); la
sociedad del grupo con la que se firma la dice el contrato, y es la que hay que buscar. El método de esa lección sirve
para comprobarlo con este bróker y con cualquier otro.

## TWS o IB Gateway

Según la documentación de la TWS API, desde el punto de vista de un programa **IB Gateway y TWS son idénticos**: los
dos son el servidor al que se abre el socket después de que el usuario se ha autenticado, y en los dos hay que escribir
usuario y contraseña en una ventana de inicio de sesión (una sesión sin interfaz gráfica no está soportada). Gateway es
más ligero (consume alrededor de un 40 % menos de recursos); TWS añade las herramientas gráficas de la plataforma[^1].

| | TWS | IB Gateway |
|---|---|---|
| Puertos por defecto (real / papel) | 7496 / 7497 | 4001 / 4002 |
| Interfaz | Plataforma completa | Ventana mínima |
| Recursos | Más | Alrededor de un 40 % menos |
| Uso típico | Desarrollo, supervisión manual | Servidor de un sistema automático |

Lo que más condiciona a un sistema no es la elección sino el **ciclo de vida** que comparten:

- **Reinicio diario.** Los dos están diseñados para reiniciarse cada día (entre otras cosas, para volver a descargar
  las definiciones de contratos). Desde la versión 974 tienen un reinicio automático sin intervención; con él pueden
  funcionar de domingo a domingo sin volver a autenticarse, y tras el reinicio de los servidores del sábado por la
  noche hay que introducir de nuevo las credenciales[^1]. La página de reautenticación recomienda a los usuarios de la
  API las opciones «Never lock Trader Workstation» y «Auto restart», y explica el mensaje que aparece cuando toca la
  reautenticación semanal[^2].
- **Mantenimiento de los servidores.** La documentación de los códigos de sistema avisa de que es muy probable perder
  la conexión con los servidores al menos una vez al día por el mantenimiento diario, que TWS o Gateway se reconectan
  solos después y que no se recomienda operar durante esas ventanas[^3].
- **Inicio de sesión automático.** Como la sesión exige una ventana, los sistemas desatendidos han usado herramientas
  de terceros que rellenan el usuario y la contraseña y gestionan los reinicios. La más conocida, IBC, en la que se
  apoya la clase `Watchdog` de `ib_async`, anunció su retirada el 1 de septiembre de 2026: su repositorio está
  archivado, sin nuevas funciones ni soporte activo[^4]. Quien la use hereda ese riesgo de mantenimiento.

```mermaid
stateDiagram-v2
  [*] --> Conectado
  Conectado --> SinServidor: error 1100
  SinServidor --> Conectado: 1102 datos intactos
  SinServidor --> Resuscribir: 1101 datos perdidos
  Resuscribir --> Conectado
  Conectado --> SinSocket: reinicio diario
  SinSocket --> Conectado: reconectar y reconciliar
```

Los mensajes 1100 (se perdió la conexión entre TWS y los servidores), 1101 (restaurada, con las suscripciones de datos
perdidas: hay que volver a pedirlas) y 1102 (restaurada, con los datos mantenidos) llegan como errores, pero no son
consecuencia de nada que haya hecho el programa[^3]. Cuando el que se cae es el propio TWS o Gateway, se pierde el
socket: `ib_async` emite `disconnectedEvent` y, al volver a conectar, resincroniza su estado. El programa tiene que
reconstruir el suyo: la última sección lo hace.

## ib_async 2.1.0: el modelo de programación

`ib_async` es la continuación de `ib_insync` (lección 1.3.6). La versión 2.1.0 se publicó en PyPI el 8 de diciembre de
2025 y era la última el 22 de octubre de 2026; requiere Python 3.10 o superior y depende de `aeventkit`, `nest_asyncio`
y `tzdata` (con la restricción `tzdata<2026.0`)[^5]. **No está instalada en el entorno del curso**: con la versión de
`tzdata` del entorno (2026.5), añadirla obligaría a bajar `tzdata`, y para ejecutarla hace falta una sesión de TWS o
Gateway. Por eso el código real va en bloques sin ejecutar y, para ejecutar algo, la lección construye un IB simulado
con la misma forma.

```python
import importlib.util
import sys

print("Python", sys.version.split()[0])
print("ib_async instalado:", importlib.util.find_spec("ib_async")
      is not None)
```
```output
Python 3.14.8
ib_async instalado: False
```

El código real de una conexión, tal como lo documenta la versión 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, papel
           timeout=10, readonly=False)
print(ib.isConnected(), ib.managedAccounts())
print(ib.positions())                         # ya sincronizadas
ib.sleep(1)                                   # nunca time.sleep
ib.disconnect()
# En Jupyter, antes de nada: util.startLoop()
```

Tres ideas sostienen la librería:

- **API síncrona sobre asyncio.** `connect`, `qualifyContracts` o `reqExecutions` bloquean hasta tener la respuesta,
  pero por debajo hay un bucle de asyncio que sigue procesando mensajes. Mientras el programa espera, ese bucle tiene
  que correr: por eso se espera con `ib.sleep()` o `ib.run()` y nunca con `time.sleep()`, que lo congela (la propia
  documentación de `util.sleep` lo dice). Cada método tiene su versión `…Async` para usarlo con `await`.
- **Objetos vivos.** `placeOrder` devuelve un `Trade` y `reqMktData` un `Ticker`, que se van actualizando solos a
  medida que llegan mensajes: `trade.orderStatus.status`, `trade.fills`, `ticker.bid`.
- **Eventos.** El objeto `IB` y cada `Trade` exponen eventos (`orderStatusEvent`, `execDetailsEvent`,
  `commissionReportEvent`, `errorEvent`, `disconnectedEvent`; `trade.filledEvent`, `trade.fillEvent`…) a los que se
  conecta una función con `+=`. La documentación desaconseja hacer peticiones nuevas dentro de un manejador de eventos.

El simulador reproduce esas tres ideas con las mismas clases y campos que la versión 2.1.0 (comprobados en su código):
`Contract`, `Order`, `OrderStatus`, `Execution`, `CommissionReport`, `Fill`, `Trade` y un `Event` mínimo.

```python
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from typing import NamedTuple

class Event:
    """Lo esencial de eventkit.Event: += conecta, emit llama."""
    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']
```

Son subconjuntos de las clases reales (que tienen muchos más campos), con los mismos nombres. El IB simulado mantiene
un **reloj lógico**: los mensajes se programan con un retraso en milisegundos y `sleep` los entrega en orden, sin mirar
el reloj del sistema ni dormir de verdad, así que la salida es siempre la misma. Sus métodos públicos tienen la misma
firma que los de `ib_async` (`connect`, `qualifyContracts`, `reqMarketDataType`, `reqMktData`, `placeOrder`, `sleep`,
`reqExecutions`); los que empiezan por `_` hacen de TWS y del bróker.

```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:
    """Interfaz de ib_async.IB sin red: reloj lógico y mensajes
    programados como los que enviarían TWS o IB Gateway."""
    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))

    # reloj lógico
    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()

    # contratos, como qualifyContracts de la versión 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":   # AAAAMM o AAAAMMDD
            return getattr(d, k).startswith(v)
        return getattr(d, k) == v

    # datos de mercado
    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

    # órdenes
    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:          # se crea en TWS, no se envía
            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

    # lo que hacen TWS y el bróker
    def _status(self, tr, cambios):
        if tr.isDone():                  # un estado final no cambia
            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                       # nadie escucha, o repetida
        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)
```

## Contratos: calificar antes de usar

Un `Contract` describe un instrumento. La documentación de IBKR da dos formas mínimas de identificarlo sin ambigüedad:
el **conId** (el identificador único de IBKR) con el destino (`exchange`), o el símbolo, el tipo, el destino, la bolsa
principal (`primaryExchange`) y la divisa; los derivados necesitan además el vencimiento, el multiplicador o la clase de
negociación[^6]. `qualifyContracts` pide los detalles al servidor y rellena el contrato **en el sitio**, conId incluido.
El código real:

```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"),                 # ¿qué divisa?
           Stock("ORSC", "SMART", "USD"),
           Future("QES", "202603", "CME")]         # mes AAAAMM
calificados = ib.qualifyContracts(*pedidos)
for pedido, c in zip(pedidos, calificados):
    if c is None:                                  # 2.1.0: None
        print("sin calificar:", pedido)
    else:
        print(c.conId, c.localSymbol, c.primaryExchange)
```

En la versión 2.1.0 el resultado mantiene una posición por contrato pedido y pone `None` en los que no se pueden
calificar, sea porque no existen o porque son ambiguos (`qualifyContractsAsync(..., returnAll=True)` devuelve en ese
hueco la lista de candidatos); antes, la ambigüedad solo se anotaba en el registro. El simulador tiene un catálogo
ficticio con el ETF `ORSC` de los datos del módulo 4.5, un homónimo en dólares canadienses y dos vencimientos de un
futuro inventado:

```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` sin divisa es ambiguo (dos acciones con el mismo símbolo) y vuelve `None` sin error; con la divisa se califica
con su conId y su bolsa principal, conservando `SMART` como destino (el enrutador de IBKR). El futuro pedido por mes,
`202603`, se resuelve al contrato de marzo, con su símbolo local y su multiplicador, que hay que usar para calcular el
nominal (lección 3.1.1). El símbolo que no existe vuelve `None` y, además, un error 200 («No security definition has
been found»)[^7]. Dos costumbres que evitan problemas: guardar el **conId** de cada instrumento en la configuración del
sistema (es estable y no ambiguo) y comprobar `None` antes de enviar nada.

## Datos de mercado: saber qué se está mirando

`reqMktData` devuelve un `Ticker` vacío que se rellena a medida que llegan ticks. Sin suscripción a los datos del
mercado correspondiente (lección 1.3.6), IBKR no envía datos en tiempo real por la API; se puede pedir otro **tipo de
datos** con `reqMarketDataType`[^8]:

| Tipo | Nombre | Qué es, según IBKR |
|---|---|---|
| 1 | Live | Datos en tiempo real; exigen suscripción |
| 2 | Frozen | La última cotización registrada al cierre; misma suscripción que el tipo 1 |
| 3 | Delayed | Datos gratuitos con 15-20 minutos de retraso |
| 4 | Delayed frozen | Los diferidos del cierre para quien no tiene suscripción |

El `Ticker` guarda el tipo en `marketDataType`. Ese campo es la defensa contra un error caro: un sistema que pierde
la suscripción (o que arranca en una máquina nueva con la configuración por defecto) puede seguir recibiendo precios
diferidos y operar con ellos sin enterarse.

```python skip title="ib_datos.py"
ib.reqMarketDataType(3)                       # diferidos si no hay 1
ticker = ib.reqMktData(contrato, "", False, False)
ib.sleep(2)                                   # se va rellenando
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)              # tipo 1 sin suscripción
ib.sleep(1)
print("tipo 1:", errores, t1.midpoint())
ib.reqMarketDataType(3)
t3 = ib.reqMktData(qes)              # diferidos
ib.sleep(1)
print("tipo 3:", t3.marketDataType, t3.bid, t3.ask, t3.midpoint())

def puedo_operar(tk):
    """Solo con datos en tiempo real y con las dos puntas."""
    return tk.marketDataType == 1 and tk.hasBidAsk()
print("¿operar con estos precios?", puedo_operar(t3))
```
```output
tipo 1: [(354, 'Requested market data is not subscribed')] nan
tipo 3: 3 5401.5 5401.75 5401.625
¿operar con estos precios? False
```

Con el tipo 1 y sin suscripción llega el error 354 («Requested market data is not subscribed») y el ticker se queda
vacío (`nan`); con el tipo 3 llegan bid y ask, pero diferidos. La regla `puedo_operar` los rechaza. En un sistema real
la regla va en el gestor de riesgo (lección 4.5.1) y se acompaña de otra: un tope de antigüedad del último tick.

## Órdenes, estados y eventos

`placeOrder(contrato, orden)` devuelve un `Trade` en estado `PendingSubmit` y el resto llega por mensajes. Los estados
que documenta IBKR[^9]:

```mermaid
stateDiagram-v2
  [*] --> PendingSubmit
  PendingSubmit --> PreSubmitted
  PendingSubmit --> Submitted
  PreSubmitted --> Submitted
  Submitted --> Filled
  Submitted --> PendingCancel
  PendingCancel --> Cancelled
  Submitted --> Cancelled
  PendingSubmit --> Inactive
```

`PreSubmitted` es una orden aceptada que aún no se ha activado (una orden simulada por IBKR, o la hija de un bracket
hasta que se ejecuta la padre); `Submitted`, una orden activa en su destino; `PendingCancel`, una cancelación pedida y
no confirmada, durante la cual **todavía puede llegar una ejecución**. Una cancelación puede llegar también sin que el
programa la pida, cuando el destino rechaza la orden: los códigos 201 («Order rejected») y 202 («Order cancelled»)
explican el motivo[^7]. `ib_async` considera terminados los estados `Filled`, `Cancelled`, `ApiCancelled` e `Inactive`
(`trade.isDone()`).

Cada ejecución llega como `execDetails` con un objeto `Execution` (`execId`, `time`, `side` `BOT` o `SLD`, `shares`,
`price`, `cumQty`, `avgPrice`, `orderRef`…), y su comisión después, en un `commissionReport` con el mismo `execId`[^10].
La TWS API actual llama a ese objeto `CommissionAndFeesReport`; `ib_async` 2.1.0 mantiene el nombre
`CommissionReport` y el evento `commissionReportEvent`. El código real de un sistema que se suscribe a los eventos:

```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("completa",
                                    t.orderStatus.avgFillPrice)
ib.run()                                       # deja correr el bucle
```

El simulador reproduce la orden **R00005** de la cuenta real del módulo 4.5: compra a mercado de 434 `ORSC` con tres
ejecuciones, con los mensajes de `brokers4_ejemplos.json`. Antes de usar ese fichero hay que comprobar sus campos
contra la documentación: los nombres de `orderStatus`, `execution` y `commissionReport` coinciden con las clases de
`ib_async` 2.1.0, pero sus `execId` tienen tres segmentos (`0000e0d5.710000035.01`) donde IBKR documenta cuatro, con el
último reservado a las correcciones (`….01` y su corrección `….02`)[^10]. Leído al pie de la letra, el segundo
`execId` del fichero sería una corrección del primero. El guion añade el cuarto segmento. Los tiempos de las
ejecuciones son los del fichero; el envío (30 ms) es la diferencia entre `created_at` y `submitted_at` de la misma
orden en el mensaje de Alpaca, y los 2 ms entre la ejecución y su comisión son un supuesto del simulador.

```python
import json

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

def guion_r00005(ib, tr, ej, t_envio, retraso_cr_ms=2):
    """Programa en el simulador los mensajes de la orden R00005."""
    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 segmentos (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("terminada:", tr.isDone(), "precio medio:",
      tr.orderStatus.avgFillPrice, "comisión:",
      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
terminada: True precio medio: 234.8353 comisión: 2.06
```

La figura de la cabecera dibuja esa secuencia. La orden se acepta a los 30 ms; la primera ejecución (27 acciones)
llega a los 219 ms; la segunda, la grande (389), a los 1,7 s; y la última (18) a los 3,6 s, con el estado `Filled`. El
precio medio, 234,8353, y la comisión total, 2,06 dólares, son los del fichero. Tres detalles que un sistema tiene que
respetar:

- **El orden de los mensajes.** La ejecución llega antes que el estado que la refleja y la comisión después. Un
  manejador que calcule el coste de la orden en `orderStatusEvent` todavía no tiene la comisión.
- **El estado no basta.** `Submitted` con `filled = 416` es una orden parcialmente ejecutada: la posición ya cambió.
  Las posiciones se llevan con las ejecuciones (`execDetails`), no con los estados.
- **El identificador propio.** `orderRef` viaja con la orden y vuelve en cada `Execution`: es lo que permite reconocer
  las órdenes propias tras una reconexión. La referencia de la API lo describe como «intended for institutional
  customers only», pero la del objeto `Execution` lo presenta como una cadena configurable desde la API que acompaña a
  la orden toda su vida[^10]; conviene comprobar en la cuenta de papel que se conserva.

## Bracket: una orden con su stop o ninguna

Una orden de entrada con su toma de beneficios y su stop (lección 1.1.4) se envía en IBKR como un **bracket**: la
padre y dos hijas con `parentId`. `IB.bracketOrder` de `ib_async` las crea con `transmit=False` en la padre y en la toma
de beneficios y `transmit=True` en el stop. La referencia de la API define el campo: si es falso, la orden se crea en
TWS pero no se envía[^11]. Así el grupo sale entero cuando llega la última, y si el programa se cae a mitad, no sale
nada.

```python skip title="ib_bracket.py"
bracket = ib.bracketOrder("BUY", 100, limitPrice=234.80,
                          takeProfitPrice=240.00,
                          stopLossPrice=230.00)
for o in bracket:                       # padre, beneficio, stop
    o.orderRef = "R00999"
    ib.placeOrder(orsc, o)
```

El simulador retiene las órdenes con `transmit=False` y envía el grupo con la que llega con `transmit=True`. La celda
compara el bracket correcto con el error de enviar la padre con `transmit=True` y caerse antes de las hijas:

```python
def bracket(ib, accion, q, lmt, beneficio, stop):
    """Como IB.bracketOrder de ib_async (ids consecutivos)."""
    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("antes del stop:", estados(ib))
ib.placeOrder(orsc, stop)
ib.sleep(1)
print("con el 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                  # el error
ib2.placeOrder(orsc, padre)            # ...y el programa se cae
ib2.sleep(1)
print("padre sola:    ", estados(ib2))
```
```output
antes del stop: {1: 'PendingSubmit', 2: 'PendingSubmit'}
con el stop:    {1: 'Submitted', 2: 'PreSubmitted', 3: 'PreSubmitted'}
padre sola:     {1: 'Submitted'}
```

Con el bracket correcto, la padre y la toma de beneficios esperan en `PendingSubmit` (creadas, no enviadas) hasta
que llega el stop; entonces sale el grupo: la padre activa (`Submitted`) y las hijas en `PreSubmitted`, esperando a
que se ejecute la padre. En el segundo caso la padre sale sola y queda activa: si se ejecuta, la posición no tiene stop.
El mismo razonamiento vale para cualquier grupo de órdenes que deba salir junto (las OCA, *one cancels all*, que
`ib_async` arma con `IB.oneCancelsAll`).

## Reconexión y reconciliación

Una desconexión a mitad de una orden es lo normal, no la excepción: reinicio diario, mantenimiento, red. Mientras el
programa no escucha, el bróker sigue ejecutando. Al reconectar hay que pedir las ejecuciones (`reqExecutions`, que
también acepta un filtro `ExecutionFilter`) y las órdenes abiertas (`reqAllOpenOrders`) y **fusionarlas** con lo que el
programa ya tenía, con dos reglas de la documentación de IBKR: cada ejecución parcial tiene su `execId`, y una
corrección llega como otra ejecución con los mismos datos salvo el `execId`, que difiere solo en las cifras tras el
último punto[^10]. `ib_async`, además, borra todo su estado interno al desconectar y lo vuelve a pedir al conectar
(posiciones, órdenes abiertas y completadas, ejecuciones), así que el libro propio del sistema es el que hay que
reconciliar.

```python skip title="ib_reconexion.py"
from ib_async import ExecutionFilter

def al_error(req_id, codigo, texto, contrato):
    if codigo == 1100:
        print("sin conexión con los servidores")
    elif codigo == 1101:                 # datos perdidos
        resuscribir_datos()
    elif codigo == 1102:                 # datos mantenidos
        reconciliar()

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

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

En el simulador, la API se desconecta al segundo de enviar R00005 (un reinicio de Gateway), la segunda ejecución
llega mientras nadie escucha y, ya reconectado, el bróker publica una **corrección** del precio de la tercera:

```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)                   # se cae Gateway
ib.programa(2500, ib.connect, "127.0.0.1", 4002, 7)
ib.sleep(4)
# 50 s después, el bróker corrige el precio de la tercera
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):
    """Una ejecución por base de execId: gana la última corrección."""
    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("solo lo escuchado:  ", posicion(libro.values()))
recibidas = [f.execution for f in ib.reqExecutions()]
print("reqExecutions:      ", len(recibidas), "mensajes")
sumadas = sum(e.shares for e in recibidas)
print("sumando todo:       ", sumadas, "acciones")
print("fusionado y con la corrección:", posicion(recibidas))
```
```output
solo lo escuchado:   (45.0, 234.716, 2)
reqExecutions:       4 mensajes
sumando todo:        452.0 acciones
fusionado y con la corrección: (434.0, 234.8361, 3)
```

Lo que el programa escuchó le da 45 acciones (la primera y la tercera ejecución): la grande se perdió durante la
desconexión. `reqExecutions` devuelve cuatro mensajes, las tres ejecuciones y la corrección; sumarlos sin más da 452
acciones, 18 de más. Fusionados por `execId`, con la corrección sustituyendo a la tercera, la posición es la verdadera:
434 acciones a 234,8361 de precio medio (la corrección subió dos céntimos el precio de 18 acciones). Este es el mismo
problema que las lecciones 4.5.2 y 4.5.5 resuelven en general con los registros del sistema en vivo (ejecuciones
duplicadas, perdidas y corregidas); aquí con las reglas concretas de IBKR.

## Límites de ritmo

La lección 1.3.6 explicó los límites de IBKR para datos históricos. Para todas las peticiones, la documentación fija un
máximo de peticiones por segundo igual a las líneas de datos de mercado divididas entre dos: con las 100 líneas por
defecto, **50 por segundo**; por encima, algunas órdenes pueden quedar en cola y retrasarse[^12]. `ib_async` frena por
su cuenta: su cliente limita a 45 peticiones por cada segundo (`MaxRequests = 45`, `RequestsInterval = 1`), por debajo
del límite. Lo que eso significa para una ráfaga de órdenes:

```python
def salidas(n, maximo=45, ventana=1.0):
    """Momento de salida de n peticiones lanzadas a la vez en 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("salen por segundo:", por_segundo)
print(f"la última sale a los {s[-1]:.1f} s")
```
```output
salen por segundo: {0: 45, 1: 45, 2: 30}
la última sale a los 2.0 s
```

*Fig. — Ráfaga de 120 peticiones lanzadas a la vez: peticiones enviadas acumuladas con el freno de ib_async (45 por segundo) frente al máximo de IBKR con 100 líneas de datos (50 por segundo).* Dos escaleras crecientes de 0 a 120 peticiones en 2 segundos. La de ib_async, destacada, sube a 45 en t = 0, a 90 en t = 1 s y a 120 en t = 2 s; la del límite de IBKR, discontinua, sube a 50, 100 y 120 en los mismos instantes.

Las 120 peticiones tardan dos segundos en salir: 45, 45 y 30. Para el sistema del módulo 4.5, que rebalancea diez ETF
una vez al día, es irrelevante. Para uno que reacciona a muchos instrumentos a la vez (o que cancela y reenvía órdenes
en bucle), dos segundos de cola cambian los precios a los que se ejecuta, y el orden de envío importa: primero las
órdenes que reducen riesgo.

## Errores típicos

- **Usar `time.sleep`.** Congela el bucle de `ib_async`: no llegan estados ni ejecuciones mientras duerme.
- **No comprobar `None` tras `qualifyContracts`.** En la versión 2.1.0 un contrato ambiguo no lanza una excepción.
- **Operar con datos diferidos.** Hay que comprobar `ticker.marketDataType` y la antigüedad del último tick.
- **Llevar la posición con los estados.** La posición sale de las ejecuciones, fusionadas por `execId`.
- **Enviar la padre de un bracket con `transmit=True`.** Si el programa se cae, queda una posición sin stop.
- **Usar el mismo `clientId` en dos programas.** IBKR rechaza la segunda conexión (error 326); además, cada conexión
  solo ve sus propias órdenes salvo la del *master client ID* configurado en TWS[^13].
- **Olvidar el reinicio diario.** Un sistema que no se reconecta solo deja de operar una vez al día.

## Ejercicios

1. Modifica el guion de R00005 para que el bróker rechace la orden (estado `Cancelled` con el error 201 a los 20 ms,
   sin ejecuciones) y comprueba qué devuelve `tr.isDone()` y qué recibe un manejador conectado a `errorEvent`.

**Solución.**

```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: (simulado)", 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)]
```

La orden termina (`isDone()` es verdadero) en `Cancelled`, sin ejecuciones, y el error llega con el `orderId` como
identificador de la petición: así se relaciona el motivo con la orden. Un sistema no debe reintentar a ciegas un
rechazo; el texto del error (tamaño, precio fuera de rango, margen) dice si tiene sentido.

2. Con la función `posicion`, ¿qué precio medio y cuántas ejecuciones darías por buenas si la corrección de la tercera
   ejecución llegara **antes** que la ejecución original (fuera de orden)?

**Solución.**

```python
desordenadas = [recibidas[0], recibidas[1], recibidas[3],
                recibidas[2]]
print(posicion(desordenadas))
```
```output
(434.0, 234.8361, 3)
```

El resultado no cambia: la función se queda, para cada base de `execId`, con el sufijo de corrección más alto, no con
el último en llegar. Una versión que se quedara con el último mensaje recibido daría el precio sin corregir. Los
identificadores, no el orden de llegada, deciden.

3. Un sistema conectado a IB Gateway en una cuenta de papel recibe el mensaje 1101 a las 6:00 de la mañana y, a las
   9:30, ninguna cotización nueva. ¿Qué ha pasado y qué debería haber hecho?

**Solución.**

El 1101 significa que la conexión entre Gateway y los servidores se restauró pero las suscripciones de datos se
perdieron y hay que volver a pedirlas. Si el programa no las pidió, sus `Ticker` se quedaron con los últimos valores
anteriores al corte y no se actualizan. Debería haber vuelto a llamar a `reqMktData` para cada contrato al recibir el
1101 (con el 1102 no hace falta) y vigilar la antigüedad del último tick: un precio que no cambia en horas de mercado
abierto es una alarma, no un mercado tranquilo.

[^1]: Interactive Brokers, TWS API, [The IB Gateway](https://www.interactivebrokers.com/docs/tws-api/doc/architecture/the-trader-workstation/the-ib-gateway),
    consultada el 22/10/2026.
[^2]: Interactive Brokers, TWS API, [Daily & Weekly Reauthentication](https://www.interactivebrokers.com/docs/tws-api/doc/tws-settings/daily-weekly-reauthentication),
    consultada el 22/10/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 y 1300), consultada el 22/10/2026.
[^4]: IBC, [README del repositorio](https://github.com/IbcAlpha/IBC) («This project has been retired, as of 1
    September 2026»); `ib_async` 2.1.0, clase `Watchdog` (código fuente), consultados el 22/10/2026.
[^5]: [ib_async en PyPI](https://pypi.org/project/ib_async/) (versión 2.1.0, publicada el 08/12/2025; dependencias
    `aeventkit>=2.1.0,<3.0.0`, `nest_asyncio` y `tzdata>=2025.2,<2026.0`) y
    [documentación de la API](https://ib-api-reloaded.github.io/ib_async/api.html), consultadas el 22/10/2026.
[^6]: Interactive Brokers, TWS API, [The Contract Object](https://www.interactivebrokers.com/docs/tws-api/doc/contracts-financial-instruments/the-contract-object),
    consultada el 22/10/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), consultada el 22/10/2026.
[^8]: Interactive Brokers, TWS API, [Delayed Market Data](https://www.interactivebrokers.com/docs/tws-api/doc/md/delayed-data)
    (tipos de datos 1 a 4), consultada el 22/10/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),
    consultada el 22/10/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)
    y [Commission And Fees Report](https://www.interactivebrokers.com/docs/tws-api/doc/order-management/commission-and-fees-report),
    consultadas el 22/10/2026.
[^11]: Interactive Brokers, TWS API, referencia del objeto [Order](https://www.interactivebrokers.com/docs/tws-api/ref/order)
    (`transmit`, `parentId`, `ocaGroup`, `orderRef`), consultada el 22/10/2026.
[^12]: Interactive Brokers, TWS API, [Pacing Limitations](https://www.interactivebrokers.com/docs/tws-api/doc/pacing-limitations/introduction);
    `ib_async` 2.1.0, clase `Client` (código fuente), consultados el 22/10/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),
    consultada el 22/10/2026.

## Autoevaluación

1. Según la documentación de la TWS API, ¿qué diferencia hay entre TWS e IB Gateway para un programa que usa la API?

   - A. IB Gateway tiene una API más completa
   - B. Para la API son idénticos: los dos son el servidor al que se abre el socket tras autenticarse; Gateway no tiene las herramientas gráficas y consume unos recursos un 40 % menores
   - C. TWS no admite conexiones de la API
   - D. IB Gateway no necesita usuario ni contraseña

   *Respuesta B: Para la API son idénticos: los dos son el servidor al que se abre el socket tras autenticarse; Gateway no tiene las herramientas gráficas y consume unos recursos un 40 % menores.* La documentación dice que, desde el punto de vista de la API, IB Gateway y TWS son idénticos, que en los dos hay que introducir usuario y contraseña en una ventana de inicio de sesión y que Gateway es más ligero (alrededor de un 40 % menos de recursos).

2. Tras `ib.qualifyContracts(Stock('ORSC', 'SMART'))`, la lista devuelta contiene `None`. ¿Qué ha pasado en ib_async 2.1.0?

   - A. La conexión ha fallado
   - B. El contrato está calificado y None es su conId
   - C. El contrato es desconocido o ambiguo (por ejemplo, el mismo símbolo en dos divisas); hay que añadir campos (currency, primaryExchange) o usar el conId
   - D. Hay que esperar más tiempo

   *Respuesta C: El contrato es desconocido o ambiguo (por ejemplo, el mismo símbolo en dos divisas); hay que añadir campos (currency, primaryExchange) o usar el conId.* En la versión 2.1.0, el resultado mantiene la posición de cada contrato pedido y pone None en los que no se pueden calificar. Antes solo se avisaba en el registro. Un sistema que no comprueba ese None envía órdenes con un contrato incompleto.

3. Un sistema sin suscripción a los datos de un futuro llama a `ib.reqMarketDataType(3)` y recibe un `Ticker` con bid y ask. ¿Qué debe comprobar antes de operar con esos precios?

   - A. `ticker.marketDataType`: 3 significa datos diferidos (15-20 minutos según la documentación), que no sirven para decidir una orden
   - B. Nada: si hay bid y ask, son buenos
   - C. Que el bid sea menor que el ask
   - D. El número de líneas de datos

   *Respuesta A: `ticker.marketDataType`: 3 significa datos diferidos (15-20 minutos según la documentación), que no sirven para decidir una orden.* Con el tipo 3, IBKR envía datos diferidos con sus propios tipos de tick. El Ticker de ib_async guarda el tipo en marketDataType. Una regla de seguridad sencilla es no generar órdenes si el tipo no es 1 (datos en tiempo real).

4. En una orden bracket de ib_async, la orden padre y la de toma de beneficios van con `transmit=False` y la de stop con `transmit=True`. ¿Por qué?

   - A. Por compatibilidad con versiones antiguas
   - B. Para que la orden de stop se ejecute primero
   - C. Porque el stop no admite transmit=False
   - D. Porque una orden con transmit=False se crea en TWS sin enviarse; al llegar la última con transmit=True se envía el grupo entero, así que nunca queda la padre activa sin sus hijas

   *Respuesta D: Porque una orden con transmit=False se crea en TWS sin enviarse; al llegar la última con transmit=True se envía el grupo entero, así que nunca queda la padre activa sin sus hijas.* La referencia de la API define transmit: si es False, la orden se crea en TWS pero no se envía. El bracket de ib_async aprovecha eso para que el grupo salga junto. Si el programa se cae entre la padre y las hijas, no sale nada; si la padre fuera con transmit=True, podría ejecutarse sin stop.

5. Tras una reconexión, `reqExecutions()` devuelve una ejecución con execId `…035.03.02` y otra con `…035.03.01`, iguales salvo el precio. ¿Qué indica la documentación de IBKR?

   - A. Son dos ejecuciones distintas: hay que sumar las dos
   - B. La .02 es una corrección de la .01: el execId solo difiere en las cifras tras el último punto, y sustituye a la anterior
   - C. Es un error del bróker que hay que ignorar
   - D. La .01 es la buena porque llegó antes

   *Respuesta B: La .02 es una corrección de la .01: el execId solo difiere en las cifras tras el último punto, y sustituye a la anterior.* La documentación de ExecID dice que una corrección llega como otro execDetails con los mismos parámetros salvo el execId, que difiere solo en las cifras tras el último punto. Contarlas como dos duplica la posición; quedarse con la primera deja un precio que el bróker ya ha corregido.

## Ideas clave

- TWS e IB Gateway son, para la API, el mismo servidor local; Gateway es más ligero. Los dos se reinician cada día (con reinicio automático desde la versión 974) y piden credenciales de nuevo cada semana; un sistema tiene que contar con esas desconexiones.
- ib_async 2.1.0 (última versión en PyPI, 08/12/2025) envuelve la TWS API en asyncio: llamadas síncronas, objetos Trade y Ticker que se actualizan solos y eventos. Se espera con ib.sleep o ib.run, nunca con time.sleep.
- Calificar contratos rellena el conId; en 2.1.0 un contrato ambiguo o desconocido vuelve como None en su posición, y hay que comprobarlo antes de usarlo.
- El tipo de datos de mercado (1 real, 2 congelado, 3 diferido, 4 diferido congelado) viaja en el Ticker: un sistema no debe operar con datos que no sean de tipo 1.
- Los estados de una orden (PendingSubmit, PreSubmitted, Submitted, Filled, Cancelled, Inactive) llegan por orderStatus; las ejecuciones por execDetails y su comisión por commissionReport, después. El execId identifica cada ejecución y sus correcciones.
- Tras una desconexión, el estado se reconstruye pidiendo ejecuciones y órdenes al bróker y fusionándolas por execId con lo que ya se tenía: sin duplicar y aplicando las correcciones.

## Recursos

### Documentación

- [ib_async 2.1.0: documentación de la API (IB, Trade, Ticker, eventos)](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 (tipos de datos 1 a 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

### Datos

- [Datos del nivel 4: mensajes de ejemplo de brókeres, orden R00005 (JSON)](https://pythonparatrading.com/curso/datos/nivel-4/brokers4_ejemplos.json) · Descarga · brokers4_ejemplos.json

Lección anterior: [4.4.1 · Elegir bróker desde España o la UE: regulación, activos, costes y API](https://pythonparatrading.com/curso-python-trading/nivel-4/elegir-broker-desde-espana-y-la-ue/)

Lección siguiente: [4.4.3 · Alpaca, Tradier, MetaTrader 5 con Python y OANDA v20](https://pythonparatrading.com/curso-python-trading/nivel-4/alpaca-tradier-metatrader-5-y-oanda/)
