IBKR adapter

Interactive Brokers via ib_async. Requires a locally-running TWS or IB Gateway.

horizon.venues.ibkr.Ibkr wraps ib_async (the maintained fork of ib_insync). Unlike every other venue in Horizon, IBKR has no hosted HTTPS endpoint. A locally-running TWS (desktop app) or IB Gateway (headless Java app) holds the authenticated session and exposes a TCP socket the Python process connects to.

The class exposes requires_local_gateway = True so higher layers can surface a useful warning when the gateway is not running.

Deployment envelope (the hard part)

Before the adapter can do anything useful, the following must be running:

  1. TWS or IB Gateway with API access enabled, logged in, and the correct paper/live profile.
  2. IBC (community autologin daemon) to keep the gateway logged in across the mandatory daily restarts.
  3. A supervisor (systemd, Docker with a restart policy, supervisord) to relaunch the gateway if it crashes.
  4. Market-data subscriptions purchased on IBKR’s Market Data page if you need real-time quotes. Delayed data is free (reqMarketDataType(3)).

A typical prod layout: Docker image with ibgateway + IBC + xvfb + supervisord. The Python process talks to host.docker.internal:4002 (paper) or 4001 (live).

Ports

ClientPortNotes
TWS paper7497Default.
TWS live7496
IB Gateway paper4002Headless.
IB Gateway live4001

Each concurrent Python process needs a unique client_id on the gateway (1..999). client_id=0 receives manually-entered TWS orders.

Quickstart

python
from horizon.venues.ibkr import Ibkr

ibkr = Ibkr(host="127.0.0.1", port=7497, client_id=1, paper=True)
ibkr.connect()                           # raises if the gateway isn't running
print(ibkr.balance())

No API keys. The gateway login (username, password, 2FA) is managed by IBC at the gateway process level, not in the Python code.

Paper / live safety valve

Ibkr(paper: bool = True) is mandatory paired with port. The constructor cross-checks against the known IBKR port pairs and refuses to construct on a mismatch — a typo on port= can otherwise silently route real capital because IBKR has no in-band paper/live flag.

python
Ibkr(port=7497, paper=True)    # OK  — paper TWS
Ibkr(port=4002, paper=True)    # OK  — paper Gateway
Ibkr(port=7496, paper=False)   # OK  — live TWS, EXPLICIT
Ibkr(port=4001, paper=False)   # OK  — live Gateway, EXPLICIT

Ibkr(port=7496, paper=True)    # ValueError: port 7496 is a known IBKR LIVE port
Ibkr(port=7497, paper=False)   # ValueError: port 7497 is a known IBKR PAPER port

Unknown / custom ports (e.g. a tunnelled gateway) are allowed but emit a WARNING log so the operator can confirm intent.

Connection health probe

Ibkr.health_check() issues a cheap reqCurrentTime round-trip against the gateway and detects the silent TCP-half-open failure mode where isConnected() still returns True for minutes after the gateway has died.

python
if not ibkr.health_check():
    # ibkr.is_connected() is now False; next call will reconnect.
    raise RuntimeError("IBKR gateway unresponsive")

print(ibkr._last_health_rtt_s)   # last measured RTT in seconds
print(ibkr._last_health_at)      # tz-aware UTC datetime

Call from your run loop every 5–30 seconds in production. The method is rate-limited through the same TokenBucket (default 45 req/s, burst 10) so it cannot pace-violate the gateway by itself.

Symbology

Market idContract
"AAPL"Stock(AAPL, SMART, USD)
"OPT:AAPL:20260619:180:C"Option (OCC-style expiry, strike, right)
"FUT:ES:202503:CME"Future (symbol, YYYYMM, exchange)
"FX:EUR.USD"Forex

Contracts are qualified on first use (qualifyContracts resolves the conId).

Order types

TypeOrderAction.order_typeIB mapping
Market"market"MKT
Limit"limit"LMT
Stop"stop"STP with auxPrice=stop_price
Stop-limit"stop_limit"STP LMT with auxPrice + lmtPrice
Trailing-stop"trailing_stop"TRAIL with trailingPercent or auxPrice

TIF maps to DAY, GTC, IOC, OPG, GTD. Bracket and OCO orders are roadmap (use ib.bracketOrder() directly until then).

Status map

IBKR statuses map to OrderStatus:

IB statusOrderStatus
PendingSubmit, PreSubmitted, ApiPendingPendingNew
SubmittedAccepted
Submitted with filled > 0PartiallyFilled
FilledFilled
Cancelled, ApiCancelledCanceled
PendingCancelPendingCancel
InactiveRejected

Fills

A fill becomes “final” when execDetailsEvent + commissionReportEvent have both fired for the same execId, with orderStatus=Filled. The adapter:

  1. Captures the execution on execDetailsEvent (price, shares, side, venue) and pushes a VenueFill into the buffer.
  2. Refines the buffered fill’s fee and fee_currency when commissionReportEvent arrives for the matching execId.

drain_fills() returns everything buffered since the last call. Fills dedup by execId.

Financial advisor accounts

ib.managedAccounts() lists sub-accounts. The adapter picks the first as the default when account= is not passed. For block trading across sub-accounts, set order.account per sub-account or use faGroup / faProfile on the IB Order for allocation; see Allocation for how this fits into Horizon’s block-allocation flow.

Gotchas

  • Scheduled restart. TWS restarts daily around midnight exchange-local; IB Gateway restarts weekly. The socket drops. IBC handles the relaunch; the adapter must reconnect. Wire a supervisor that restarts the Python process too.
  • Timezones. IBKR returns exchange-local timestamps. The adapter normalizes to UTC at the boundary.
  • Real-time data subscriptions. Without them, reqMktData returns delayed quotes. NBBO fat-finger checks and best-ex reporting need real-time data.
  • clientId collisions. Two processes with the same clientId will kick each other. Reserve ranges per service.

Status by phase

CapabilityStatus
Connect via ib_async, auto-disconnect on closeShipped
Stock, Option, Future, Forex symbologyShipped
Submit (market, limit, stop, stop_limit, trailing)Shipped
Cancel, cancel_all (per-symbol), amendShipped
Positions, balance (via accountSummary), open_ordersShipped
Fills via execDetailsEvent + commissionReportEventShipped
Paper / live safety valve (paper= cross-checks port=)Shipped
Rate limiter (TokenBucket, 45 req/s default, tunable)Shipped
health_check() via reqCurrentTimeShipped
Structured logging on every silent-except pathShipped
Bracket / OCO orders as one ticketRoadmap
Real-time quotes (reqMktData)Roadmap
Managed account enumerationAuto-selects first
Financial-advisor allocation groupsL2

Related