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.
- Módulo
- 4.4 · Brokers y APIs
- Duración
- 70 min
Lecciones del nivel 4
4.1 · Teoría y optimización de carteras
- 4.1.1 Markowitz: frontera eficiente y restricciones reales
- 4.1.2 Estimación de covarianzas: shrinkage, modelos factoriales y denoising
- 4.1.3 Black-Litterman: del equilibrio del mercado a las visiones
- 4.1.4 Risk parity, mínima varianza, HRP y HERC
- 4.1.5 Optimización con CVaR, drawdown, rotación y costes
- 4.1.6 Modelos factoriales: CAPM, Fama-French, PCA y atribución
- 4.1.7 Rebalanceo y carteras de estrategias
- 4.1.8 Herramientas de carteras: Riskfolio-Lib, PyPortfolioOpt, skfolio y bt
4.2 · Gestión del riesgo y tamaño de posición
- 4.2.1 Riesgo por operación, stops y riesgo de ruina
- 4.2.2 Criterio de Kelly y volatility targeting: cuánto arriesgar
- 4.2.3 VaR, Expected Shortfall y backtesting del VaR
- 4.2.4 Stress testing y riesgos de correlación, liquidez, gap y contraparte
- 4.2.5 Control del drawdown y kill switch
- 4.2.6 Riesgo de derivados: griegas de cartera, margen y liquidaciones
4.3 · Microestructura y ejecución
4.4 · Brokers y APIs
- 4.4.1 Elegir bróker desde España o la UE: regulación, activos, costes y API
- 4.4.2 Interactive Brokers: TWS e IB Gateway, ib_async, contratos, datos y órdenes
- 4.4.3 Alpaca, Tradier, MetaTrader 5 con Python y OANDA v20
- 4.4.4 APIs de exchanges cripto: ccxt, Binance, Bybit, Kraken y Coinbase
- 4.4.5 Credenciales seguras: variables de entorno, gestores de secretos y permisos mínimos
4.5 · Sistema de trading en vivo
- 4.5.1 Arquitectura de un sistema en vivo: de los datos a la reconciliación
- 4.5.2 Gestión de órdenes: estados, fills parciales, cancelaciones y reconexiones
- 4.5.3 Paper trading, shadow trading y seguimiento del slippage
- 4.5.4 Programación de tareas y alertas: cron, APScheduler, Prefect, Telegram, Slack y email
- 4.5.5 Tolerancia a fallos: idempotencia, estado persistente y reconciliación al arrancar
- 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..
| 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 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
Watchdogdeib_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_async2.1.0, claseWatchdog(código fuente), consultados el 22/10/2026.. Quien la use hereda ese riesgo de mantenimiento.
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.
import importlib.util
import sys
print("Python", sys.version.split()[0])
print("ib_async instalado:", importlib.util.find_spec("ib_async")
is not None)Python 3.14.8
ib_async instalado: FalseEl código real de una conexión, tal como lo documenta la versión 2.1.0:
# 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,qualifyContractsoreqExecutionsbloquean 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 conib.sleep()oib.run()y nunca contime.sleep(), que lo congela (la propia documentación deutil.sleeplo dice). Cada método tiene su versión…Asyncpara usarlo conawait. - Objetos vivos.
placeOrderdevuelve unTradeyreqMktDataunTicker, que se van actualizando solos a medida que llegan mensajes:trade.orderStatus.status,trade.fills,ticker.bid. - Eventos. El objeto
IBy cadaTradeexponen 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.
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])['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.
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:
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:
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)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.:
| 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.
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)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))tipo 1: [(354, 'Requested market data is not subscribed')] nan
tipo 3: 3 5401.5 5401.75 5401.625
¿operar con estos precios? FalseCon 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.:
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:
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 bucleEl 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.
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)) 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.06La 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
orderStatusEventtodavía no tiene la comisión. - El estado no basta.
Submittedconfilled = 416es una orden parcialmente ejecutada: la posición ya cambió. Las posiciones se llevan con las ejecuciones (execDetails), no con los estados. - El identificador propio.
orderRefviaja con la orden y vuelve en cadaExecution: 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 objetoExecutionlo 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.
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:
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))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.
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 orderRefEn 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:
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))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:
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")salen por segundo: {0: 45, 1: 45, 2: 30}
la última sale a los 2.0 sLas 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 deib_async: no llegan estados ni ejecuciones mientras duerme. - No comprobar
NonetrasqualifyContracts. En la versión 2.1.0 un contrato ambiguo no lanza una excepción. - Operar con datos diferidos. Hay que comprobar
ticker.marketDataTypey 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
clientIden 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
- Modifica el guion de R00005 para que el bróker rechace la orden (estado
Cancelledcon el error 201 a los 20 ms, sin ejecuciones) y comprueba qué devuelvetr.isDone()y qué recibe un manejador conectado aerrorEvent.
Solución
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)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.
- 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
desordenadas = [recibidas[0], recibidas[1], recibidas[3],
recibidas[2]]
print(posicion(desordenadas))(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.
- 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
-
Interactive Brokers, TWS API, The IB Gateway, consultada el 22/10/2026. ↩ ↩2
-
Interactive Brokers, TWS API, Daily & Weekly Reauthentication, consultada el 22/10/2026. ↩
-
Interactive Brokers, TWS API, System Message Codes (1100, 1101, 1102 y 1300), consultada el 22/10/2026. ↩ ↩2
-
IBC, README del repositorio («This project has been retired, as of 1 September 2026»);
ib_async2.1.0, claseWatchdog(código fuente), consultados el 22/10/2026. ↩ -
ib_async en PyPI (versión 2.1.0, publicada el 08/12/2025; dependencias
aeventkit>=2.1.0,<3.0.0,nest_asyncioytzdata>=2025.2,<2026.0) y documentación de la API, consultadas el 22/10/2026. ↩ -
Interactive Brokers, TWS API, The Contract Object, consultada el 22/10/2026. ↩
-
Interactive Brokers, TWS API, Error Codes (200, 201, 202, 326, 354), consultada el 22/10/2026. ↩ ↩2
-
Interactive Brokers, TWS API, Delayed Market Data (tipos de datos 1 a 4), consultada el 22/10/2026. ↩
-
Interactive Brokers, TWS API, Understanding Order Status Message, consultada el 22/10/2026. ↩
-
Interactive Brokers, TWS API, The Execution Object, ExecID Behavior y Commission And Fees Report, consultadas el 22/10/2026. ↩ ↩2 ↩3 ↩4
-
Interactive Brokers, TWS API, referencia del objeto Order (
transmit,parentId,ocaGroup,orderRef), consultada el 22/10/2026. ↩ -
Interactive Brokers, TWS API, Pacing Limitations;
ib_async2.1.0, claseClient(código fuente), consultados el 22/10/2026. ↩ -
Interactive Brokers, TWS API, ClientId 0 and the Master Client ID, consultada el 22/10/2026. ↩
Respuesta plegada en cada pregunta
Autoevaluación
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).
Tras
ib.qualifyContracts(Stock('ORSC', 'SMART')), la lista devuelta contieneNone. ¿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.
Un sistema sin suscripción a los datos de un futuro llama a
ib.reqMarketDataType(3)y recibe unTickercon 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 - 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 ordenCon 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).
- A
En una orden bracket de ib_async, la orden padre y la de toma de beneficios van con
transmit=Falsey la de stop contransmit=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.
Tras una reconexión,
reqExecutions()devuelve una ejecución con execId…035.03.02y 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.