Skip to content

Interactive Brokers SDK

Complete reference for ctx.ibkr — programmatic stocks, options, futures, forex, and bonds trading on Interactive Brokers from worker code.

Architectural notes

IBKR is the architectural odd-one-out among our trading integrations:

  1. No API keys, no signed requests. Auth is session-based — the IBKR Client Portal Gateway runs on the user's machine at https://localhost:5000 and the user logs in there with their IBKR username + password.
  2. The worker reaches the user's gateway over the public internet. This means the user's machine must be on, the gateway must be running, and the gateway URL must be reachable (port-forwarding or tunnel, e.g. tailscale).
  3. conid not symbol. IBKR identifies every instrument by a numeric Contract ID. AAPL = 265598, IBM = 8314, etc. Use search_contract or get_contract_details to discover.
  4. Daily reset. All iserver/* endpoints are unavailable for a few minutes around 01:00 in the user's region as IBKR resets the brokerage session.

19 methods. Server-only.

Setup

  1. Install Client Portal Gateway on the machine that will run the worker (or any machine the worker can reach):
    • Download from interactivebrokers.com/en/trading/ib-api.php
    • Run with bin/run.sh root/conf.yaml (Linux/Mac) or bin\run.bat root\conf.yaml (Windows)
    • Open https://localhost:5000 in a browser, accept the self-signed cert, log in with your IBKR credentials.
  2. Add an IBKR block to your workspace canvas.
  3. Configure the gateway URL in the inspector — https://localhost:5000 if the worker runs on the same machine, or your public/tailnet URL for remote workers.
  4. Connect the block to your Worker block via an edge.
  5. ctx.ibkr is now available.
python
def tick(ctx):
    accs = ctx.ibkr.get_accounts()
    ctx.log.info(f"Accounts: {[a['accountId'] for a in accs]}")

If no IBKR block is connected:

RuntimeError: No trading block connected. Connect an IBKR block to this worker.

conid Discovery

Every IBKR endpoint identifies instruments by conid. Two ways to find it:

search_contract

Search by ticker + security type. Fast for stocks, options, futures, forex.

python
ctx.ibkr.search_contract(symbol: str, sec_type: str = "STK") -> dict
# sec_type: "STK" | "OPT" | "FUT" | "CASH" (forex) | "BOND" | "CFD" | "FUND" | "IND" | "WAR"
python
res = ctx.ibkr.search_contract("AAPL", "STK")
conid = res[0]["conid"]   # 265598

get_contract_details

Full metadata for a known conid: trading rules, exchanges, symbology, increments.

python
ctx.ibkr.get_contract_details(conid: int) -> dict

Session Management

These ensure the brokerage session is alive (IBKR sessions die after ~30 min of inactivity).

tickle

Keepalive ping. Call every few minutes from a long-running worker.

python
ctx.ibkr.tickle() -> dict

auth_status

Check if the brokerage session is authenticated.

python
ctx.ibkr.auth_status() -> dict
# returns {"authenticated": true|false, "competing": false, "connected": true, "MAC": "..."}

select_account

Set the active trading account for the session. Required after login when the user has multiple accounts (cash + margin, or sub-accounts).

python
ctx.ibkr.select_account(account_id: str) -> dict
python
def setup(ctx):
    ctx.ibkr.tickle()
    accs = ctx.ibkr.get_accounts()
    ctx.ibkr.select_account(accs[0]["accountId"])

Account & Portfolio

get_accounts

All accounts visible to the logged-in user.

python
ctx.ibkr.get_accounts() -> list

get_subaccounts

For Financial Advisor (FA) or IBroker users: their managed sub-accounts.

python
ctx.ibkr.get_subaccounts() -> list

get_summary

High-level account summary: equity, available funds, P&L, margin.

python
ctx.ibkr.get_summary(account_id: str) -> dict

get_ledger

Multi-currency balance ledger — cashbalance, netliquidationvalue, unrealizedpnl, stockmarketvalue, margin per currency.

python
ctx.ibkr.get_ledger(account_id: str) -> dict
python
ledger = ctx.ibkr.get_ledger("U1234567")
usd = ledger.get("USD", {})
ctx.log.info(f"USD cash: ${usd.get('cashbalance', 0):,.2f}, "
             f"NLV: ${usd.get('netliquidationvalue', 0):,.2f}")

get_positions

All open positions for an account.

python
ctx.ibkr.get_positions(account_id: str) -> list

Market Data

get_market_snapshot

Top-of-book quotes for one or many conids.

python
ctx.ibkr.get_market_snapshot(conids, fields: list = None) -> list
FieldTagDescription
Last31Last traded price
Bid84Best bid
Ask86Best ask
BidSize88Best-bid size
AskSize85Best-ask size
LastSize7059Last trade size
Volume87Today's volume

IBKR's preflight quirk

The first snapshot request after a fresh session is a preflight — it initiates the streaming pipeline and returns no prices. The second request returns actual data. Pattern:

python
ctx.ibkr.get_market_snapshot(conids=[265598])
import time; time.sleep(0.5)
quote = ctx.ibkr.get_market_snapshot(conids=[265598])
last = float(quote[0].get("31", 0))

get_historical_data

Historical OHLC bars + volume.

python
ctx.ibkr.get_historical_data(
    conid: int,
    period: str = "1d",          # "{n}{unit}" e.g. "1d", "1w", "3m", "1y"
    bar: str = "5min",           # bar size: "1min", "5min", "1h", "1d", ...
    outside_rth: bool = False,
) -> dict
python
hist = ctx.ibkr.get_historical_data(conid=265598, period="1d", bar="5min")
closes = [b["c"] for b in hist["data"]]
sma_20 = sum(closes[-20:]) / 20

Quota

IBKR allows max 5 concurrent historical-data requests per session. Don't fan out across 100 conids in one tick.

Orders

get_orders

All orders (any status) for the session.

python
ctx.ibkr.get_orders() -> dict

get_filled_orders

Same shape but server-filtered to filled orders only.

python
ctx.ibkr.get_filled_orders(account_id: str = None) -> dict

place_order

Place a single order.

python
ctx.ibkr.place_order(
    account_id: str,
    conid: int,
    side: str,                     # "BUY" | "SELL"
    quantity: float,
    order_type: str = "MKT",       # "MKT" | "LMT" | "STP" | "STP_LMT" | "TRAIL"
    price: float = None,           # required for LMT, STP_LMT
    tif: str = "DAY",              # "DAY" | "GTC" | "IOC" | "OPG" | "FOK"
) -> dict
python
# Market buy 10 shares of AAPL
r = ctx.ibkr.place_order(
    account_id="U1234567",
    conid=265598, side="BUY", quantity=10, order_type="MKT",
)

IBKR fat-finger replies

IBKR sometimes responds with a confirmation prompt instead of placing the order ("Are you sure you want to buy at >5% above last?"). The response will be a list with id (the message id) and message. Call reply_to_order(message_id, confirmed=True) to proceed, or suppress_messages([id]) once at startup to skip these warnings for the whole session.

modify_order

Modify an unfilled order. Pass all original fields plus the change — IBKR replaces the order rather than patching.

python
ctx.ibkr.modify_order(
    account_id: str,
    order_id: str,
    conid: int,
    side: str,
    quantity: float,
    order_type: str = "LMT",
    price: float = None,
    tif: str = "DAY",
) -> dict

cancel_order

Cancel an order. Returns acknowledgement of receipt — not confirmation that the cancel filled.

python
ctx.ibkr.cancel_order(account_id: str, order_id: str) -> dict

reply_to_order

Reply to a fat-finger / large-order confirmation message.

python
ctx.ibkr.reply_to_order(message_id: str, confirmed: bool = True) -> dict
python
def tick(ctx):
    r = ctx.ibkr.place_order(account_id, conid, "BUY", 1000, "MKT")
    if isinstance(r, list) and r and "message" in r[0]:
        # Fat-finger warning
        ctx.log.warn(f"IBKR: {r[0]['message']}")
        ctx.ibkr.reply_to_order(r[0]["id"], confirmed=True)

suppress_messages

Suppress one or more fat-finger message types for the remainder of this session. Useful in pure-bot workflows where you don't want manual confirmations.

python
ctx.ibkr.suppress_messages(message_ids: list) -> dict
python
def setup(ctx):
    ctx.ibkr.suppress_messages(["o163"])   # large-order warning

Common Patterns

Keepalive

IBKR sessions die silently after ~30 minutes of inactivity. Tickle every tick:

python
def tick(ctx):
    ctx.ibkr.tickle()
    # ... rest of strategy

Daily reset awareness

Brokerage endpoints (everything iserver/*) are unavailable for ~5 minutes around 01:00 in the user's region. Plan strategies that don't trade during this window.

python
from datetime import datetime, timezone

def tick(ctx):
    now = datetime.now(timezone.utc)
    if now.hour == 5 and now.minute < 10:    # rough US/Eastern 01:00 in UTC
        ctx.log.info("IBKR daily reset window — skipping")
        return
    # ... trade

Error handling

python
def tick(ctx):
    try:
        positions = ctx.ibkr.get_positions(account_id)
    except Exception as e:
        msg = str(e)
        if "competing" in msg.lower():
            ctx.log.error("Another IBKR session is competing — aborting")
            return
        ctx.log.error(f"IBKR unreachable: {e}")
        return

IbkrAPIError carries code and msg attributes. Most non-200s come back as JSON with an error field that the SDK extracts.

Cloud-Run proxy

IBKR traffic does not route through the Cloud Run proxy — the gateway is on the user's machine, not a public IP. Workers connect directly. The verify=False SSL flag is set because the gateway uses a self-signed cert.

Recipes

Multi-account daily NLV summary

python
def setup(ctx):
    ctx.ibkr.tickle()

def tick(ctx):
    accs = ctx.ibkr.get_accounts()
    total_usd = 0
    for a in accs:
        ledger = ctx.ibkr.get_ledger(a["accountId"])
        usd = ledger.get("USD", {})
        nlv = float(usd.get("netliquidationvalue", 0))
        total_usd += nlv
        ctx.log.info(f"{a['accountId']:10} NLV ${nlv:,.2f}")
    ctx.monitor.metric("ibkr_total_nlv", total_usd)

Mean-reversion on AAPL with bracket exits

python
def setup(ctx):
    ctx.ibkr.tickle()
    # Skip fat-finger prompts for the session
    ctx.ibkr.suppress_messages(["o163", "o451"])
    # Cache the conid
    res = ctx.ibkr.search_contract("AAPL", "STK")
    ctx.state.set("aapl_conid", int(res[0]["conid"]))
    ctx.state.set("account", ctx.ibkr.get_accounts()[0]["accountId"])

def tick(ctx):
    ctx.ibkr.tickle()
    conid = ctx.state.get("aapl_conid")
    acct = ctx.state.get("account")

    hist = ctx.ibkr.get_historical_data(conid=conid, period="1d", bar="15min")
    closes = [b["c"] for b in hist["data"]]
    if len(closes) < 20:
        return
    sma = sum(closes[-20:]) / 20
    last = closes[-1]

    pos = ctx.ibkr.get_positions(acct)
    has = any(p["conid"] == conid and p["position"] != 0 for p in pos)

    if last < sma * 0.99 and not has:
        r = ctx.ibkr.place_order(acct, conid, "BUY", 10, "MKT")
        ctx.log.info(f"BUY 10 AAPL @ {last:.2f} (SMA20 {sma:.2f})")
        # If IBKR asks for confirmation, auto-confirm
        if isinstance(r, list) and r and "message" in r[0]:
            ctx.ibkr.reply_to_order(r[0]["id"], confirmed=True)
    elif last > sma * 1.02 and has:
        ctx.ibkr.place_order(acct, conid, "SELL", 10, "MKT")
        ctx.log.info(f"SELL 10 AAPL @ {last:.2f}")

Working-order ladder around current price

python
def setup(ctx):
    ctx.ibkr.tickle()
    ctx.ibkr.suppress_messages(["o163"])
    ctx.state.set("conid", 265598)
    ctx.state.set("account", ctx.ibkr.get_accounts()[0]["accountId"])

def tick(ctx):
    ctx.ibkr.tickle()
    conid = ctx.state.get("conid")
    acct = ctx.state.get("account")

    # Cancel any leftovers
    open_orders = ctx.ibkr.get_orders().get("orders", [])
    for o in open_orders:
        if o.get("conid") == conid and o.get("status") in ("PreSubmitted", "Submitted"):
            ctx.ibkr.cancel_order(acct, o["orderId"])

    # Re-quote: 5 buy rungs, $0.50 apart
    snap = ctx.ibkr.get_market_snapshot(conids=[conid])
    import time; time.sleep(0.4)
    snap = ctx.ibkr.get_market_snapshot(conids=[conid])
    if not snap or "31" not in snap[0]:
        return
    last = float(snap[0]["31"])

    for i in range(1, 6):
        ctx.ibkr.place_order(
            account_id=acct, conid=conid, side="BUY",
            quantity=10, order_type="LMT",
            price=round(last - i * 0.5, 2), tif="DAY",
        )
    ctx.log.info(f"Buy ladder placed below {last:.2f}")

Reference

IBKR endpointSDK method
POST /tickletickle
POST /iserver/auth/statusauth_status
POST /iserver/accountselect_account
GET /portfolio/accountsget_accounts
GET /portfolio/subaccountsget_subaccounts
GET /portfolio/{id}/summaryget_summary
GET /portfolio/{id}/ledgerget_ledger
GET /portfolio/{id}/positions/0get_positions
GET /iserver/marketdata/snapshotget_market_snapshot
GET /iserver/marketdata/historyget_historical_data
GET /iserver/secdef/searchsearch_contract
GET /iserver/contract/{conid}/infoget_contract_details
GET /iserver/account/ordersget_orders / get_filled_orders
POST /iserver/account/{id}/ordersplace_order
POST /iserver/account/{id}/order/{oid}modify_order / cancel_order
POST /iserver/reply/{messageId}reply_to_order
POST /iserver/questions/suppresssuppress_messages

AiSpinner Documentation