Lección 4.4.2 · 70 min

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.

Fig. — La orden R00005 (compra a mercado de 434 ORSC) mensaje a mensaje en el IB simulado: estados, ejecuciones e informes de comisión, en milisegundos desde placeOrder
Lecciones del nivel 4
  1. 4.1 · Teoría y optimización de carteras

    1. 4.1.1 Markowitz: frontera eficiente y restricciones reales
    2. 4.1.2 Estimación de covarianzas: shrinkage, modelos factoriales y denoising
    3. 4.1.3 Black-Litterman: del equilibrio del mercado a las visiones
    4. 4.1.4 Risk parity, mínima varianza, HRP y HERC
    5. 4.1.5 Optimización con CVaR, drawdown, rotación y costes
    6. 4.1.6 Modelos factoriales: CAPM, Fama-French, PCA y atribución
    7. 4.1.7 Rebalanceo y carteras de estrategias
    8. 4.1.8 Herramientas de carteras: Riskfolio-Lib, PyPortfolioOpt, skfolio y bt
  2. 4.2 · Gestión del riesgo y tamaño de posición

    1. 4.2.1 Riesgo por operación, stops y riesgo de ruina
    2. 4.2.2 Criterio de Kelly y volatility targeting: cuánto arriesgar
    3. 4.2.3 VaR, Expected Shortfall y backtesting del VaR
    4. 4.2.4 Stress testing y riesgos de correlación, liquidez, gap y contraparte
    5. 4.2.5 Control del drawdown y kill switch
    6. 4.2.6 Riesgo de derivados: griegas de cartera, margen y liquidaciones
  3. 4.3 · Microestructura y ejecución

    1. 4.3.1 Implementation shortfall, VWAP, TWAP y Almgren-Chriss
    2. 4.3.2 Order flow imbalance, señales de microestructura y latencia
  4. 4.4 · Brokers y APIs

    1. 4.4.1 Elegir bróker desde España o la UE: regulación, activos, costes y API
    2. 4.4.2 Interactive Brokers: TWS e IB Gateway, ib_async, contratos, datos y órdenes
    3. 4.4.3 Alpaca, Tradier, MetaTrader 5 con Python y OANDA v20
    4. 4.4.4 APIs de exchanges cripto: ccxt, Binance, Bybit, Kraken y Coinbase
    5. 4.4.5 Credenciales seguras: variables de entorno, gestores de secretos y permisos mínimos
  5. 4.5 · Sistema de trading en vivo

    1. 4.5.1 Arquitectura de un sistema en vivo: de los datos a la reconciliación
    2. 4.5.2 Gestión de órdenes: estados, fills parciales, cancelaciones y reconexiones
    3. 4.5.3 Paper trading, shadow trading y seguimiento del slippage
    4. 4.5.4 Programación de tareas y alertas: cron, APScheduler, Prefect, Telegram, Slack y email
    5. 4.5.5 Tolerancia a fallos: idempotencia, estado persistente y reconciliación al arrancar
    6. 4.5.6 NautilusTrader o LEAN en real: del backtest a producción con el mismo código

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 plataforma1Nota 1 · Interactive Brokers, TWS API, The IB Gateway, consultada el 22/10/2026..

TWSIB Gateway
Puertos por defecto (real / papel)7496 / 74974001 / 4002
InterfazPlataforma completaVentana mínima
RecursosMásAlrededor de un 40 % menos
Uso típicoDesarrollo, supervisión manualServidor 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 credenciales1. 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 semanal2Nota 2 · Interactive Brokers, TWS API, Daily & Weekly Reauthentication, consultada el 22/10/2026..
  • 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 ventanas3Nota 3 · Interactive Brokers, TWS API, System Message Codes (1100, 1101, 1102 y 1300), consultada el 22/10/2026..
  • 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 activo4Nota 4 · IBC, README del repositorio («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.. Quien la use hereda ese riesgo de mantenimiento.
error 11001102 datos intactos1101 datos perdidosreinicio diarioreconectar y reconciliarConectadoSinServidorResuscribirSinSocket
Fuente 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 programa3. 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)5Nota 5 · ib_async en PyPI (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, consultadas el 22/10/2026.. 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.

IN [01]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)
OUT [01]¿Está ib_async en el entorno?
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:

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

ib = IB()
ib.connect("127.0.0.1", 4002, clientId=7,    # IB Gateway, 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.

IN [03]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])
OUT [03]Las piezas de ib_async, en miniatura
['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.

IN [04]python
import heapq

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

class IBSimulado:
    """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ón6Nota 6 · Interactive Brokers, TWS API, The Contract Object, consultada el 22/10/2026.. qualifyContracts pide los detalles al servidor y rellena el contrato en el sitio, conId incluido. El código real:

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

ib = IB()
ib.connect("127.0.0.1", 4002, clientId=7)
pedidos = [Stock("ORSC", "SMART"),                 # ¿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:

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

pedidos = [Stock("ORSC", "SMART"), Stock("ORSC", "SMART", "USD"),
           Contract("FUT", symbol="QES",
                    lastTradeDateOrContractMonth="202603"),
           Stock("ORSQ", "SMART", "USD")]
for c in ib.qualifyContracts(*pedidos):
    print(None if c is None else
          (c.conId, c.localSymbol, c.primaryExchange or c.exchange,
           c.currency, c.multiplier))
print(errores)
OUT [06]Calificar contratos: ambigüedad, divisa y vencimiento
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»)7Nota 7 · Interactive Brokers, TWS API, Error Codes (200, 201, 202, 326, 354), consultada el 22/10/2026.. 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 reqMarketDataType8Nota 8 · Interactive Brokers, TWS API, Delayed Market Data (tipos de datos 1 a 4), consultada el 22/10/2026.:

TipoNombreQué es, según IBKR
1LiveDatos en tiempo real; exigen suscripción
2FrozenLa última cotización registrada al cierre; misma suscripción que el tipo 1
3DelayedDatos gratuitos con 15-20 minutos de retraso
4Delayed frozenLos 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.

IN [07]ib_datos.pypython
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)
IN [08]python
cot = {700201: [(200, 5401.25, 5401.50), (900, 5401.50, 5401.75)]}
ib = IBSimulado(catalogo, T0, suscritos={700101}, cotizaciones=cot)
errores = []
ib.errorEvent += lambda rid, cod, txt, c: errores.append((cod, txt))
ib.connect(clientId=7)
qes = ib.qualifyContracts(Contract(
    "FUT", symbol="QES", lastTradeDateOrContractMonth="202603"))[0]

t1 = ib.reqMktData(qes)              # 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))
OUT [08]Datos reales, diferidos y una regla de seguridad
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 IBKR9Nota 9 · Interactive Brokers, TWS API, Understanding Order Status Message, consultada el 22/10/2026.:

PendingSubmitPreSubmittedSubmittedFilledPendingCancelCancelledInactive
Fuente 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 motivo7. 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 execId10Nota 10 · Interactive Brokers, TWS API, The Execution Object, ExecID Behavior y Commission And Fees Report, consultadas el 22/10/2026.. 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:

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

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

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

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

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

ib.orderStatusEvent += al_estado
ib.execDetailsEvent += al_ejecutar
ib.commissionReportEvent += a_la_comision
trade = ib.placeOrder(orsc, MarketOrder("BUY", 434, orderRef="R00005"))
trade.filledEvent += lambda t: print("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.

IN [10]python
import json

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

def guion_r00005(ib, tr, ej, t_envio, retraso_cr_ms=2):
    """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))
OUT [10]La orden R00005, mensaje a mensaje
    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 vida10; 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ía11Nota 11 · Interactive Brokers, TWS API, referencia del objeto Order (transmit, parentId, ocaGroup, orderRef), consultada el 22/10/2026.. Así el grupo sale entero cuando llega la última, y si el programa se cae a mitad, no sale nada.

IN [11]ib_bracket.pypython
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:

IN [12]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))
OUT [12]El campo transmit en un bracket
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 punto10. 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.

IN [13]ib_reconexion.pypython
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:

IN [14]python
ib = IBSimulado(catalogo, T0)
ib.connect(clientId=7)
libro = {}                                   # execId -> Execution
ib.execDetailsEvent += lambda tr, f: libro.setdefault(
    f.execution.execId, f.execution)
tr = ib.placeOrder(orsc, MarketOrder("BUY", 434, orderRef="R00005"))
guion_r00005(ib, tr, ej, T0)
ib.programa(1000, ib._cae)                   # 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))
OUT [14]Desconexión, reqExecutions y una corrección
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 retrasarse12Nota 12 · Interactive Brokers, TWS API, Pacing Limitations; ib_async 2.1.0, clase Client (código fuente), consultados el 22/10/2026.. 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:

IN [15]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")
OUT [15]Una ráfaga de 120 peticiones con el freno de ib_async
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)

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 TWS13Nota 13 · Interactive Brokers, TWS API, ClientId 0 and the Master Client ID, consultada el 22/10/2026..
  • 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
IN [16]python
ib = IBSimulado(catalogo, T0)
ib.connect(clientId=7)
avisos = []
ib.errorEvent += lambda rid, cod, txt, c: avisos.append((rid, cod))
tr = ib.placeOrder(orsc, MarketOrder("BUY", 434, orderRef="R00005"))
ib.programa(20, ib.errorEvent.emit, tr.order.orderId, 201,
            "Order rejected - Reason: (simulado)", orsc)
ib.programa(20, ib._status, tr, {"status": "Cancelled"})
ib.sleep(1)
print(tr.isDone(), tr.orderStatus.status, tr.log, avisos)
OUT [16]Una orden rechazada
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.

  1. 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
IN [17]python
desordenadas = [recibidas[0], recibidas[1], recibidas[3],
                recibidas[2]]
print(posicion(desordenadas))
OUT [17]La corrección llega primero
(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.

  1. 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.

Footnotes

  1. Interactive Brokers, TWS API, The IB Gateway, consultada el 22/10/2026. ↩ ↩2

  2. Interactive Brokers, TWS API, Daily & Weekly Reauthentication, consultada el 22/10/2026. ↩

  3. Interactive Brokers, TWS API, System Message Codes (1100, 1101, 1102 y 1300), consultada el 22/10/2026. ↩ ↩2

  4. IBC, README del repositorio («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 (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, consultadas el 22/10/2026. ↩

  6. Interactive Brokers, TWS API, The Contract Object, consultada el 22/10/2026. ↩

  7. Interactive Brokers, TWS API, Error Codes (200, 201, 202, 326, 354), consultada el 22/10/2026. ↩ ↩2

  8. Interactive Brokers, TWS API, Delayed Market Data (tipos de datos 1 a 4), consultada el 22/10/2026. ↩

  9. Interactive Brokers, TWS API, Understanding Order Status Message, consultada el 22/10/2026. ↩

  10. Interactive Brokers, TWS API, The Execution Object, ExecID Behavior y Commission And Fees Report, consultadas el 22/10/2026. ↩ ↩2 ↩3 ↩4

  11. Interactive Brokers, TWS API, referencia del objeto Order (transmit, parentId, ocaGroup, orderRef), consultada el 22/10/2026. ↩

  12. Interactive Brokers, TWS API, Pacing Limitations; 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, consultada el 22/10/2026. ↩

Respuesta plegada en cada pregunta

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?

    • AIB Gateway tiene una API más completa
    • BPara 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
    • CTWS no admite conexiones de la API
    • DIB Gateway no necesita usuario ni contraseña
    Ver respuesta

    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?

    • ALa conexión ha fallado
    • BEl contrato está calificado y None es su conId
    • CEl 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
    • DHay que esperar más tiempo
    Ver respuesta

    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?

    • Aticker.marketDataType: 3 significa datos diferidos (15-20 minutos según la documentación), que no sirven para decidir una orden
    • BNada: si hay bid y ask, son buenos
    • CQue el bid sea menor que el ask
    • DEl número de líneas de datos
    Ver respuesta

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

    • APor compatibilidad con versiones antiguas
    • BPara que la orden de stop se ejecute primero
    • CPorque el stop no admite transmit=False
    • DPorque 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
    Ver respuesta

    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?

    • ASon dos ejecuciones distintas: hay que sumar las dos
    • BLa .02 es una corrección de la .01: el execId solo difiere en las cifras tras el último punto, y sustituye a la anterior
    • CEs un error del bróker que hay que ignorar
    • DLa .01 es la buena porque llegó antes
    Ver respuesta

    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