Interactive Brokers.
The deepest product breadth in retail. Stocks, options, futures, FX, bonds, mutual funds — across 150+ exchanges. Cheap commissions, brutal API ergonomics.
The honest pitch.
Interactive Brokers is what professional retail traders end up at: the lowest commissions, the most products, and the deepest international coverage. The price is the API. The official TWS API is famously verbose — request IDs, contract specifications, market-data-type modes, and decades-old quirks. Most Python automators use 'ib_insync', a small wrapper that turns TWS's callbacks into awaitable coroutines and DataFrames. The IBKR Web API (REST + WebSocket) is newer and cleaner, but still requires a Client Portal Gateway running locally with a daily login.
Auth, orders, limits.
| Auth | TWS or IB Gateway running locally (manual or via IBKR auto-restart); Web API requires Client Portal Gateway + daily 2FA. |
|---|---|
| Order API | TWS API (raw), ib_insync (Python wrapper), or REST Web API; Contract objects identify instruments. |
| Rate limits | TWS API: 50 messages/sec; Web API: 10 req/sec; pacing violations trigger temporary disconnects. |
| Sandbox | Paper account (separate login); same TWS/Gateway endpoints, port 7497 or 7496. |
| Data subscriptions | Live market data for each exchange requires a paid subscription ($0-$130/mo per exchange). |
| Geo | Worldwide; LEI required for non-US institutional, country-by-country trading restrictions for retail. |
Hello-world, but real.
ib_insync wraps TWS callbacks in asyncio. This script connects to a running TWS/Gateway, defines a Stock contract, places a bracketed market buy (parent + stop + take-profit), and waits for the fill event. You need TWS or IB Gateway running before this works.
| 1 | # pip install ib_insync |
| 2 | from ib_insync import IB, Stock, MarketOrder, StopOrder, LimitOrder |
| 3 | |
| 4 | ib = IB() |
| 5 | ib.connect("127.0.0.1", 7497, clientId=42) # 7497 = paper, 7496 = live |
| 6 | |
| 7 | contract = Stock("SPY", "SMART", "USD") |
| 8 | ib.qualifyContracts(contract) |
| 9 | |
| 10 | # Bracket: parent market BUY + child stop SELL + child take-profit SELL |
| 11 | parent = MarketOrder("BUY", 1, transmit=False, orderId=ib.client.getReqId()) |
| 12 | take_profit = LimitOrder("SELL", 1, 0.0, parentId=parent.orderId, transmit=False, |
| 13 | orderId=ib.client.getReqId()) |
| 14 | stop_loss = StopOrder("SELL", 1, 0.0, parentId=parent.orderId, transmit=True, |
| 15 | orderId=ib.client.getReqId()) |
| 16 | |
| 17 | # Set bracket prices using last trade |
| 18 | ticker = ib.reqMktData(contract) |
| 19 | ib.sleep(2) |
| 20 | px = ticker.marketPrice() or ticker.close |
| 21 | take_profit.lmtPrice = round(px * 1.01, 2) # +1% |
| 22 | stop_loss.auxPrice = round(px * 0.995, 2) # -0.5% |
| 23 | |
| 24 | for o in (parent, take_profit, stop_loss): |
| 25 | ib.placeOrder(contract, o) |
| 26 | |
| 27 | # Wait up to 10s for the parent to fill |
| 28 | ib.sleep(10) |
| 29 | print("parent status:", ib.trades()[0].orderStatus.status) |
| 30 | ib.disconnect() |
The traps everyone hits.
Real production failure modes. Sev1 = capital loss risk. Sev2 = data integrity / silent wrongness. Sev3 = developer ergonomics that bite later.
Paper port vs live port confusion
Sev1What happens. Port 7496 is live; 7497 is paper. Hardcode 7496 in dev and one slip sends your test order to a real account.
Fix. Read the port from env var. NEVER hardcode 7496 anywhere outside production deploy config. Add a check that aborts if account-type returned by IBKR doesn't match expected.
Pacing violations
Sev2What happens. Request market data for 60 contracts in a tight loop and IBKR throttles you: 'Max number of tickers reached' or temporary disconnects.
Fix. Use reqMktData with snapshot=False to stay subscribed (free), batch contract qualification, and respect the 50 msg/sec limit using ib_insync's pacing helpers.
Daily Gateway re-login
Sev2What happens. IB Gateway requires daily 2FA. Forget to log in by 06:00 ET and your strategy can't connect when the open hits.
Fix. Use ibc / ibcontroller to automate gateway restarts and 2FA via IBKR Mobile, OR run the Web API Client Portal Gateway with read-only secondary auth.
Market data subscription gaps
Sev3What happens. Trying to trade ICE futures without the ICE data subscription returns delayed or empty quotes; your strategy thinks the market is closed.
Fix. Check the subscription status of every exchange you trade. Budget $50-$150/mo per professional retail bundle if you trade multiple asset classes.
Contract ambiguity
Sev1What happens. AAPL on SMART vs NASDAQ vs ARCA may all be valid; orderQty might route to the wrong venue and fill at the wrong price.
Fix. Always qualifyContracts before placing. Specify exchange explicitly (e.g., 'NASDAQ' for AAPL), primaryExchange for ambiguous symbols, and log the resolved contract.
What to pair it with.
No platform stands alone. These are the layers that — paired with Interactive Brokers — produce production-grade automation.
| Layer | Recommended | Why |
|---|---|---|
| Connection | TWS for human + ib_insync for code OR Web API + Client Portal Gateway | ib_insync is the de-facto retail Python wrapper; Web API is the future but newer. |
| Gateway automation | ibc (IB Controller) on Linux | Restart Gateway daily, handle 2FA via IBKR Mobile push. The only way to truly automate. |
| Multi-asset research | QuantConnect or zipline-reloaded for backtests; IBKR for execution | IBKR's historical data is rate-limited. Use QC or external data for research; IBKR only for live. |
| Risk gates | Custom Python pre-trade hook + IBKR account-level max-position limits | Belt and suspenders — your code AND IBKR's account-level limits both block runaway orders. |