Imported from njohnson101/AgenticQuantSystem (
course/08_production_hardening/.claude/skills/ib-insync/SKILL.md). Install upstream withnpx skills add njohnson101/AgenticQuantSystem --skill ib-insync. Copyright stays with the author.
ib_insync Implementation
ib_insync is a high-level Python wrapper around Interactive Brokers' TWS API. It hides the callback model behind a synchronous-feeling interface backed by asyncio. This skill covers the most common implementation patterns: connecting, fetching data, placing orders, monitoring state, and reacting to events.
Maintenance status
The original ib_insync (erdewit) was archived in March 2024 after the maintainer's passing. The community fork ib_async (ib-api-reloaded/ib_async, pip install ib_async) is the actively maintained successor with an identical API surface — only the import path changes:
# Original (archived)
from ib_insync import *
# Maintained fork (recommended for new code)
from ib_async import *
Everything in this skill applies to both unless noted. Use whichever the project already depends on; default to ib_async for new projects.
Standard imports
from ib_insync import IB, Stock, Forex, Future, Option, Crypto, util
from ib_insync import MarketOrder, LimitOrder, StopOrder, StopLimitOrder
from ib_insync import * works and is common in scripts/notebooks. Prefer explicit imports in production code.
For Jupyter notebooks, call util.startLoop() once before connecting — this patches the asyncio loop to coexist with Jupyter's IPython kernel.
The One Rule — do not block the event loop
ib_insync runs an asyncio event loop in the background to receive messages from TWS/Gateway. User code must never block this loop for long. Two practical implications:
- Never use
time.sleep()— useib.sleep(seconds)instead.time.sleepfreezes the event loop;ib.sleepyields to it. - Long computations should periodically call
ib.sleep(0)to let the framework process pending messages, or run them in a separate process.
Violating this rule causes silent message loss, stale ticker data, and timeouts on order placement. It is the most common source of bugs.
Connection — TWS vs Gateway, ports, clientId
ib = IB()
ib.connect(
host="127.0.0.1",
port=7497, # see port table below
clientId=1, # must be unique per connection to the same TWS/Gateway
timeout=4,
readonly=False, # True disables order placement (safer for data-only scripts)
account="", # optional: filter updates to this account
)
# Always disconnect cleanly
try:
# ... code ...
finally:
ib.disconnect()
Port table:
| Port | Application |
|---|---|
| 7497 | TWS Paper Trading (default) |
| 7496 | TWS Live Trading |
| 4002 | IB Gateway Paper Trading |
| 4001 | IB Gateway Live Trading |
Pre-flight checklist for the TWS/Gateway side (the agent should remind the user):
- API is enabled: Configure → API → Settings → "Enable ActiveX and Socket Clients"
- Port matches the code
- "Read-only API" is unchecked if placing orders
- "Download open orders on connection" is checked
- The IP making the connection is in "Trusted IPs" (127.0.0.1 for local)
clientId must be unique across all simultaneous connections to the same TWS/Gateway. Using clientId=0 automatically binds manual TWS orders so they're visible to the API client — usually only what you want for one master client.
By default IB.RaiseRequestErrors = False — failed requests silently return empty. For development, enable raises:
IB.RaiseRequestErrors = True
Contracts — the most error-prone surface
A Contract identifies an instrument. Most bugs come from ambiguous contracts that match multiple instruments. The fix is ib.qualifyContracts(), which fills in the missing fields (especially conId) by querying TWS.
# Specialized constructors (preferred)
spy = Stock("SPY", "SMART", "USD")
intc = Stock("INTC", "SMART", "USD", primaryExchange="NASDAQ") # dual-listed
eurusd = Forex("EURUSD") # exchange="IDEALPRO" by default
es_dec = Future("ES", "20251219", "CME")
es_front = ContFuture("ES", "CME") # continuous front-month contract
spy_call = Option("SPY", "20251219", 500, "C", "SMART")
btc = Crypto("BTC", "PAXOS", "USD")
ibm_bond = Bond(secIdType="ISIN", secId="US03076KAA60")
# Always qualify before using (fills in conId, validates)
ib.qualifyContracts(spy, intc, eurusd)
print(spy.conId) # filled in
primaryExchange matters for dual-listed names. Stock("MSFT", "SMART", "USD") alone is ambiguous; add primaryExchange="NASDAQ".
Security types (secType): STK (stock/ETF), OPT (option), FUT (future), FOP (futures option), CASH (forex), CFD, IND (index), BOND, CMDTY, CRYPTO, FUND, WAR (warrant), BAG (combo).
For full constructor reference, ambiguity resolution, and combo/spread contracts (Bag), see references/contracts.md.
Pattern 1: Historical data
contract = Stock("SPY", "SMART", "USD")
ib.qualifyContracts(contract)
bars = ib.reqHistoricalData(
contract,
endDateTime="", # "" = now; or datetime/datetime.date/"yyyyMMdd HH:mm:ss"
durationStr="30 D", # "60 S", "30 D", "13 W", "6 M", "10 Y"
barSizeSetting="1 day", # see allowed values below
whatToShow="TRADES", # TRADES, MIDPOINT, BID, ASK, BID_ASK, ADJUSTED_LAST, ...
useRTH=True, # regular trading hours only
formatDate=1, # 1 = local TWS time strings, 2 = UTC tz-aware datetimes
keepUpToDate=False, # True = subscribe to live updates after the historical fill
timeout=60, # seconds before giving up
)
df = util.df(bars) # convert BarDataList → pandas DataFrame
print(df.head())
Allowed barSizeSetting: 1 secs, 5 secs, 10 secs, 15 secs, 30 secs, 1 min, 2 mins, 3 mins, 5 mins, 10 mins, 15 mins, 20 mins, 30 mins, 1 hour, 2 hours, 3 hours, 4 hours, 8 hours, 1 day, 1 week, 1 month.
Common whatToShow values:
TRADES— actual trades (volume populated)MIDPOINT— midpoint between bid and ask (no volume)BID/ASK/BID_ASK— quote-based barsADJUSTED_LAST— split- and dividend-adjusted (stocks)
Forex and crypto don't have TRADES — use MIDPOINT or BID_ASK.
For long histories, paginate by walking endDateTime backward — see references/data.md.
Template: scripts/historical_data_template.py.
Pattern 2: Live market data (tickers)
reqMktData returns a Ticker that is kept live updated. Initially empty — wait a few seconds (or use events) for it to populate.
contract = Stock("AAPL", "SMART", "USD")
ib.qualifyContracts(contract)
ticker = ib.reqMktData(contract, "", False, False)
ib.sleep(2) # let initial ticks arrive
print(f"bid={ticker.bid} ask={ticker.ask} last={ticker.last} marketPrice={ticker.marketPrice()}")
Key Ticker attributes:
bid,ask,last,close,open,high,low,volumebidSize,askSize,lastSizemarketPrice()— sane mid/last fallback methodtime— timestamp of last updatedomBids,domAsks— order book (only withreqMktDepth)tickByTicks— raw tick stream (only withreqTickByTickData)
Market data type — required for delayed/frozen data when no live subscription is available:
ib.reqMarketDataType(3) # 1=Live, 2=Frozen, 3=Delayed, 4=DelayedFrozen
Without a market data subscription, live data won't arrive on reqMktData. Setting type 3 (delayed) lets you receive 15-minute-delayed quotes for free on most US equities — useful for development.
To unsubscribe: ib.cancelMktData(contract) — must pass the same contract object used to subscribe.
Snapshot mode (one-time read):
[ticker] = ib.reqTickers(contract) # blocks until snapshot is complete
print(ticker.last)
For tick-by-tick data (every print), real-time 5-second bars, and market depth, see references/data.md.
Template: scripts/streaming_template.py.
Pattern 3: Orders — placing and monitoring
Order types are wrappers over Order:
MarketOrder("BUY", 100) # orderType="MKT"
LimitOrder("BUY", 100, lmtPrice=185.50) # orderType="LMT"
StopOrder("SELL", 100, stopPrice=180.00) # orderType="STP" (auxPrice)
StopLimitOrder("SELL", 100, lmtPrice=179.50, stopPrice=180.00)
action is "BUY" or "SELL". totalQuantity is shares/contracts/units.
Place and monitor:
contract = Stock("SPY", "SMART", "USD")
ib.qualifyContracts(contract)
order = MarketOrder("BUY", 100)
trade = ib.placeOrder(contract, order)
# Wait for the order to reach a terminal state
while not trade.isDone():
ib.sleep(1)
print(f"Status: {trade.orderStatus.status}, filled: {trade.orderStatus.filled}")
for fill in trade.fills:
print(f" {fill.execution.shares}@{fill.execution.price}")
placeOrder returns a Trade object that is kept live updated. Important attributes:
| Attribute | Meaning |
|---|---|
trade.contract |
The Contract |
trade.order |
The Order |
trade.orderStatus.status |
One of: PendingSubmit, PreSubmitted, Submitted, Filled, Cancelled, ApiCancelled, Inactive |
trade.orderStatus.filled |
Filled quantity |
trade.orderStatus.remaining |
Unfilled quantity |
trade.orderStatus.avgFillPrice |
Volume-weighted fill price |
trade.fills |
List of Fill objects (each has .execution, .commissionReport) |
trade.log |
List of TradeLogEntry (timestamped status changes) |
trade.isDone() |
True when status is in DoneStates (terminal) |
Order modification: pass the same Order object (with orderId already set) back to ib.placeOrder() after changing fields — IB treats the same orderId as a modification.
Cancel:
ib.cancelOrder(order) # specific order
ib.reqGlobalCancel() # all orders, including manually placed and other clients
Pre-trade margin/commission impact (does not place):
state = ib.whatIfOrder(contract, order)
print(state.commission, state.initMarginChange, state.maintMarginChange)
For limit/stop variants beyond the basics (TWAP, VWAP, MOC, MOO, conditions, OCA groups), see references/orders.md.
Template: scripts/place_order_template.py.
Pattern 4: Bracket orders (entry + stop + target)
ib.bracketOrder builds the three orders with transmit flags wired correctly so only the third placement actually transmits the bracket:
bracket = ib.bracketOrder(
action="BUY",
quantity=100,
limitPrice=185.50,
takeProfitPrice=195.00,
stopLossPrice=180.00,
)
for order in bracket: # parent, takeProfit, stopLoss — order matters
ib.placeOrder(contract, order)
# Monitor any leg
parent_trade = ib.trades()[-3] # or capture trades = [ib.placeOrder(c, o) for o in bracket]
Critical mechanic: the parent has transmit=False, the take-profit has transmit=False, only the stop-loss has transmit=True. Placing them in order with transmit=True only on the last one means TWS holds the bracket as a unit until the final placement, then transmits all three together. If you reorder them, only some legs transmit and the bracket is broken.
For OCA (One Cancels All) groups across multiple unrelated orders, manual transmit control, and conditional orders (price/time/margin triggers), see references/orders.md.
Template: scripts/bracket_order_template.py.
Pattern 5: Account, positions, P&L
These are all kept live-updated in the IB object after connect():
# Account values (cash, margin, equity, etc.)
for v in ib.accountSummary():
print(f"{v.tag}={v.value} {v.currency}")
# Positions (one per (account, conId, currency))
for pos in ib.positions():
print(f"{pos.contract.symbol}: {pos.position} @ avg {pos.avgCost}")
# Portfolio (positions + market value + unrealizedPnL)
for item in ib.portfolio():
print(f"{item.contract.symbol}: pos={item.position} mktValue={item.marketValue} "
f"unrealizedPnL={item.unrealizedPNL}")
# Live P&L subscriptions
account = ib.managedAccounts()[0]
pnl = ib.reqPnL(account)
ib.sleep(2)
print(f"Daily P&L: {pnl.dailyPnL}, Unrealized: {pnl.unrealizedPnL}")
For per-contract P&L (reqPnLSingle), executions/fills queries (ib.fills(), ib.executions()), and historical executions (ib.reqExecutions(filter)), see references/recipes.md.
Events — reacting to live updates
ib_insync uses eventkit for event subscription. Subscribe with +=:
def on_status(trade):
print(f"{trade.contract.symbol}: {trade.orderStatus.status}")
def on_error(reqId, errorCode, errorString, contract):
print(f"ERROR {errorCode}: {errorString} ({contract})")
def on_ticker_update(tickers):
for t in tickers:
print(f"{t.contract.symbol}: bid={t.bid} ask={t.ask}")
ib.orderStatusEvent += on_status
ib.errorEvent += on_error
ib.pendingTickersEvent += on_ticker_update
# Run the event loop
ib.run() # blocks indefinitely; Ctrl+C to stop
Per-trade events on the Trade object itself:
trade = ib.placeOrder(contract, order)
trade.statusEvent += lambda t: print(f"status: {t.orderStatus.status}")
trade.fillEvent += lambda t, f: print(f"fill: {f.execution.shares}@{f.execution.price}")
trade.filledEvent += lambda t: print("fully filled!")
trade.cancelledEvent += lambda t: print("cancelled")
Common IB-level events:
| Event | Args | Use case |
|---|---|---|
connectedEvent |
() | Resubscribe to data on reconnect |
disconnectedEvent |
() | Cleanup, alert |
errorEvent |
(reqId, errorCode, errorString, contract) | Required for any production code |
orderStatusEvent |
(trade) | Track order lifecycle |
execDetailsEvent |
(trade, fill) | React to fills |
pendingTickersEvent |
(Set[Ticker]) | Batched ticker updates |
barUpdateEvent |
(bars, hasNewBar) | Real-time bar updates |
positionEvent |
(position) | Position changes |
accountValueEvent |
(value) | Account value changes |
Don't place new requests inside an event handler — it can cause recursion. Schedule them via ib.sleep(0) or a background coroutine instead.
Important error codes to handle:
502— couldn't connect to TWS504— not connected1100— connectivity lost1102— connectivity restored (auto-handled internally)2104,2106,2158— informational ("data farm connection ok") — usually safe to ignore200— no security definition found (bad contract)201— order rejected321— error validating request
For full event-driven recipes (auto-reconnect, throttling, multi-account routing), see references/events.md.
Template: scripts/streaming_template.py.
Common gotchas — verify before delivering
time.sleep()instead ofib.sleep()— silently breaks the event loop. Production-grade lint rule: forbidtime.sleepanywhere in ib_insync code.- Wrong port. 7497 = TWS Paper, 7496 = TWS Live, 4002 = Gateway Paper, 4001 = Gateway Live. The user has misconfigured the port if
ConnectionRefusedErrorfires. clientIdcollision. Two connections with the same clientId silently break — the newer one wins, the older one disconnects. Pass distinct IDs (1, 2, 3, ...) for multiple concurrent connections.- Ambiguous contracts.
Stock("MSFT", "SMART", "USD")may resolve to multiple instruments. Always callib.qualifyContracts(contract)and addprimaryExchange="NASDAQ"for US equities. - No market data subscription. Live
reqMktDatareturns empty. Either subscribe to data in IB account billing OR callib.reqMarketDataType(3)for free 15-min delayed. - Empty
Tickerimmediately after subscription. Tickers fill in over ~1-2 seconds. Useib.sleep(2)before reading, or attachticker.updateEventfor event-driven access. reqHistoricalDatareturns empty. Common causes: contract not qualified, request outside head timestamp (useib.reqHeadTimeStamp(contract, ...)),useRTH=Trueon a 24h instrument with no RTH data, or a duration/bar combination IB doesn't support (e.g.,1 secswith30 D).endDateTimetimezone. Without a timezone, TWS uses its login timezone. Pass tz-awaredatetimeor setformatDate=2to receive UTC.- Bracket order leg ordering. Place
parent, thentakeProfit, thenstopLossin that order. Thetransmit=Trueflag is on the last leg only; reordering breaks the bracket. - Order placed but no fill arrives. Check
trade.logfor messages, and checkib.errorEvent— often the order was rejected (insufficient margin, contract not tradable through API, outside RTH withoutoutsideRth=True). ib.run()vsib.sleep()in scripts. Useib.sleep(N)to wait N seconds while the loop spins. Useib.run()only when you want to block forever waiting for events. Inside Jupyter, neither is needed (the kernel runs the loop).- Disconnect cleanup. Always wrap in
try/finallywithib.disconnect()— orphaned connections persist on TWS and exhaust client slots. util.df()on empty BarDataList returnsNone, not an empty DataFrame. Checkif bars:before converting.
Implementation checklist
Before connecting:
- TWS or Gateway is running, API enabled, port matches
-
clientIdis unique to this script -
readonly=Trueset if no orders will be placed (defensive default)
Before placing orders:
- Contract has been passed through
ib.qualifyContracts() - Order action is
"BUY"or"SELL"(not"buy"— case matters) - Prices are floats, not strings
- If outside RTH:
Order(outsideRth=True, ...) - Tested first against paper port (7497/4002) before live (7496/4001)
After running:
- All requested data was returned (
barsnot empty, ticker fields populated) - Error handler subscribed (
ib.errorEvent += handler) caught any issues -
ib.disconnect()ran in afinallyblock
Reference files
references/contracts.md— every Contract subclass, ambiguity resolution, combo/Bag spreads, options chainsreferences/orders.md— full order type catalog, OCA groups, conditional orders, FA allocationreferences/data.md— historical data pagination, market data types, tick-by-tick, real-time bars, market depth, fundamentalsreferences/events.md— event-driven patterns, auto-reconnect, throttling, multi-strategy event routingreferences/recipes.md— common patterns: P&L tracking, scanner, options chain walking, account multiplexing
Templates
scripts/connection_template.py— connect, query account, disconnect cleanlyscripts/historical_data_template.py— fetch bars, paginate, save to DataFrame/CSVscripts/place_order_template.py— market/limit order with full lifecycle monitoringscripts/bracket_order_template.py— entry + stop + target with correct transmit flagsscripts/streaming_template.py— event-driven ticker subscription with handler
Copy the template that matches the implementation plan rather than writing from scratch.