DEV Community

pickuma
pickuma

Posted on • Originally published at pickuma.com

Interactive Brokers API: A Developer's Deep Dive Into Programmatic Trading

I opened my first Interactive Brokers account because I was tired of paying $0.65 per options contract on another platform. The irony is that I stayed because of the API — not the commission schedule. IBKR's programmatic trading interface is the most capable retail broker API I have used, and also the one with the steepest learning curve. If you are a developer who wants to execute trades, stream market data, and manage a portfolio without clicking buttons in a web portal, this is the tool you will eventually end up using. The question is whether the onboarding pain is worth it for what you are trying to build.

This is what I learned integrating the IBKR API into my own research workflow over the past year — including the parts the documentation glosses over.

TWS API vs. Client Portal REST API: Pick Before You Install

IBKR offers two completely different programmatic interfaces, and choosing wrong at the start will waste weeks.

Trader Workstation (TWS) API is the original. It communicates over a raw TCP socket with a binary protocol. You run TWS or IB Gateway on your machine, and your Python script connects to localhost:7497 (paper) or localhost:7496 (live). The protocol is low-level — you send request IDs as integers, match response IDs to callbacks, and manage connection state yourself. Nobody writes against the raw socket anymore unless they are maintaining a client library.

Client Portal Web API (CPAPI) is the modern REST interface. You authenticate through IBKR's gateway (a Java process you run locally) and interact via standard HTTP endpoints. GET /v1/api/portfolio/accounts returns your account list. POST /v1/api/iserver/account/{accountId}/orders places a trade. The REST API is simpler to reason about but covers fewer instrument types — notably, it does not support futures or complex options spreads at the time of writing.

For most developer-investors, the right answer is: use the TWS API through the ib_insync Python library, which wraps the socket protocol in an async, event-driven interface that feels natural in Python. You still need TWS or IB Gateway running, but ib_insync handles the connection lifecycle, request/response matching, and callback dispatch.

Getting Started Without Losing a Weekend

The setup sequence that actually works:

pip install ib_insync
Enter fullscreen mode Exit fullscreen mode

Then download IB Gateway from IBKR's website — not TWS. IB Gateway is the headless version, lighter on resources, and does not pop up trading windows when you auto-start it. Run it with paper trading credentials (you get these when you open an IBKR account — they are separate from your live credentials):

from ib_insync import IB

ib = IB()
ib.connect('127.0.0.1', 7497, clientId=1)
print("Connected to IBKR Paper Trading")
Enter fullscreen mode Exit fullscreen mode

The clientId parameter matters more than it seems. Each client ID gets its own order book and market data subscription pool. If you run two scripts with the same client ID, IBKR will disconnect the first one without warning. Pick a convention — I use single-digit IDs for interactive scripts, double-digit for scheduled jobs — and stick to it.

warning

Paper trading and live trading share the same API interface but not the same market data. Paper accounts receive delayed data by default. To get real-time streaming in paper mode, you must subscribe to market data in your live account and enable the "Share live data with paper account" setting in Client Portal. Skip this step and your paper trading bot will make decisions on 15-minute delayed prices — which is worse than useless because it trains you on scenarios that do not exist in live markets.

Market Data Streaming That Actually Works

Streaming market data is where the IBKR API shines compared to REST-only broker APIs. Instead of polling a /quote endpoint every second and burning rate limits, you subscribe once and receive a push on every tick:

from ib_insync import Stock

contract = Stock('AAPL', 'SMART', 'USD')
ib.qualifyContracts(contract)

# Subscribe to real-time bars (5-second aggregation)
bars = ib.reqRealTimeBars(contract, 5, 'TRADES', False)

def on_bar_update(bars, has_new_bar):
    if has_new_bar:
        last = bars[-1]
        print(f"AAPL ${last.close:.2f} | VWAP ${last.wap:.2f} | Vol {last.volume}")

bars.updateEvent += on_bar_update
ib.sleep(60)  # stream for one minute
Enter fullscreen mode Exit fullscreen mode

The ib.sleep() call is not time.sleep() — it is ib_insync's event loop runner that keeps the connection alive while processing incoming messages. Without it, your script terminates immediately after subscribing because the main thread exits.

More useful than real-time bars for most workflows: historical data requests. ib.reqHistoricalData() returns a pandas DataFrame with OHLCV bars at any supported bar size. The limitation is IBKR's pacing rules — you can request roughly 60 historical requests per 10 minutes. For a portfolio scanner pulling daily bars on 200 symbols, you will need to batch and cache.

Placing Orders Programmatically

The order interface is where new users make expensive mistakes. IBKR distinguishes between order types (market, limit, stop) and order actions (buy, sell, short), and transmitting an invalid combination will not throw an error — it will silently reject the order and you will not notice until you check your order status 10 minutes later.

Safe execution pattern:

from ib_insync import MarketOrder, LimitOrder

contract = Stock('SPY', 'SMART', 'USD')
ib.qualifyContracts(contract)

# Market order — fills immediately, no price guarantee
market = MarketOrder('BUY', 10)

# Limit order — fills only at or better than specified price
limit = LimitOrder('BUY', 10, 440.50)

# Place and wait for fill confirmation
trade = ib.placeOrder(contract, limit)
while not trade.isDone():
    ib.sleep(0.5)

print(f"Order status: {trade.orderStatus.status}")
print(f"Filled at: {trade.orderStatus.avgFillPrice}")
Enter fullscreen mode Exit fullscreen mode

The while not trade.isDone() loop is critical. Do not place an order and assume it filled — market orders can receive partial fills, limit orders can sit unfilled indefinitely, and IBKR's smart routing can split a single order across multiple venues. Always check trade.orderStatus.status for 'Filled' before acting on the assumption that you own the position.

info

Never use MKT orders in after-hours or pre-market sessions. Liquidity is thin, spreads widen dramatically, and your market order can fill at prices that would make you physically ill. Use limit orders with a reasonable offset from the last close, even if it means missing a fill.

What the IBKR API Does Better Than the Competition

After using Alpaca, Robinhood's unofficial API, and TD Ameritrade's (now Schwab's) API side by side, here is where IBKR wins:

Instrument coverage. IBKR supports stocks, options, futures, forex, bonds, mutual funds, and warrants across 150+ markets in 33 countries. Alpaca does US equities and that is it. If your strategy ever extends beyond US stocks, IBKR is the only retail broker API that scales with you.

Order routing. IBKR's smart routing scans multiple exchanges and ECNs to find the best execution price. Alpaca routes everything through a single market maker. Over a year of active trading, smart routing adds up — I measured roughly 2-3 cents per share better execution on IBKR for limit orders on liquid names like SPY and QQQ compared to Alpaca.

Margin API. You can query your margin requirements, available funds, and borrowing rates programmatically. This is useful for strategies that use leverage and need to stay within risk limits — ib.accountSummary() returns your net liquidation value, initial margin, and maintenance margin in one call.

Where IBKR loses: developer experience. Alpaca's API docs are better, their Python SDK is better documented, and their paper trading environment does not require running a local Java gateway process. The cognitive overhead of IBKR's TWS setup is real, and if you are testing a simple moving-average crossover on SPY, you will ship faster on Alpaca.

FAQ


Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.

Top comments (0)