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

NodeGraph Documentation