Interactive Brokers API: Get Positions in Python (ibapi)
To get positions with the Interactive Brokers API in Python, call reqPositions(), collect each position() callback into a list, wait for positionEnd(), then build one pandas DataFrame. Use reqAccountSummary for NAV.
- reqPositions() covers every account the login can see and finishes with positionEnd(), so collect rows in a list and build the DataFrame once.
- Wait on threading.Event objects set by nextValidId, positionEnd and accountSummaryEnd instead of using time.sleep().
- Codes 2104, 2106, 2107, 2119 and 2158 are informational notices, so filter them inside error().
- Use reqAccountSummary for NAV across accounts, because reqAccountUpdates can subscribe to only one account at a time.
- For futures and options, avgCost already includes the contract multiplier, so divide by it to get a per-unit price.
How do I use the Interactive Brokers API to get positions in Python with pandas?
This script connects, waits for the session to be ready, pulls positions for all accounts, pulls NetLiquidation and cash, prints two DataFrames and exits cleanly. It uses the official ibapi package with no sleeps. Test it on a paper account first.
The error() signature matches current ibapi releases, which pass advancedOrderRejectJson as a fourth argument. The trailing *extra absorbs any further arguments a newer build may add.
import threading
import pandas as pd
from ibapi.client import EClient
from ibapi.wrapper import EWrapper
INFO_CODES = {2104, 2106, 2107, 2119, 2158}
class App(EWrapper, EClient):
def __init__(self):
EClient.__init__(self, self)
self.ready = threading.Event()
self.pos_done = threading.Event()
self.sum_done = threading.Event()
self.positions, self.summary = [], []
def nextValidId(self, orderId):
self.ready.set()
def position(self, account, contract, pos, avgCost):
self.positions.append({
"account": account, "conId": contract.conId,
"symbol": contract.symbol, "secType": contract.secType,
"currency": contract.currency, "exchange": contract.exchange,
"multiplier": contract.multiplier,
"pos": float(pos), "avgCost": avgCost})
def positionEnd(self):
self.pos_done.set()
def accountSummary(self, reqId, account, tag, value, currency):
self.summary.append({"account": account, "tag": tag,
"value": value, "currency": currency})
def accountSummaryEnd(self, reqId):
self.sum_done.set()
def error(self, reqId, errorCode, errorString,
advancedOrderRejectJson="", *extra):
if errorCode in INFO_CODES:
return
print(f"IB error reqId={reqId} code={errorCode}: {errorString}")
app = App()
app.connect("127.0.0.1", 7497, clientId=17) # 7497 = TWS paper
threading.Thread(target=app.run, daemon=True).start()
if not app.ready.wait(10):
raise SystemExit("No nextValidId: check port, API setting, clientId")
app.reqPositions()
if not app.pos_done.wait(15):
print("positionEnd timeout")
app.cancelPositions()
app.reqAccountSummary(9001, "All", "NetLiquidation,TotalCashValue,BuyingPower")
if not app.sum_done.wait(15):
print("accountSummaryEnd timeout")
app.cancelAccountSummary(9001)
app.disconnect()
pos = pd.DataFrame(app.positions)
summ = pd.DataFrame(app.summary)
print(pos)
print(summ.pivot_table(index="account", columns="tag",
values="value", aggfunc="first"))What do I need set up before the script will connect?
The API is a socket served by a running TWS or IB Gateway. Your script is only a client, so nothing works until the application is logged in and listening.
Install the official client from IBKR's own distribution (the TWS API download, then run python setup.py install in its pythonclient folder) or via pip install ibapi. Do not use any old placeholder release on PyPI that does not match your TWS version.
| Application | Mode | Default socket port |
|---|---|---|
| TWS | Live | 7496 |
| TWS | Paper | 7497 |
| IB Gateway | Live | 4001 |
| IB Gateway | Paper | 4002 |
- In TWS or Gateway open Global Configuration > API > Settings.
- Tick Enable ActiveX and Socket Clients.
- Confirm the Socket port matches your connect() call.
- Tick Read-Only API if the script only reads data. Order requests are then rejected, which protects against accidental trades.
- Add 127.0.0.1 to Trusted IPs to avoid the confirmation popup, and pick a unique clientId per script.
Why does ibapi need a thread, callbacks and events?
EClient sends requests and EWrapper receives answers. app.run() is a blocking loop that reads the socket and dispatches messages to your overridden methods, so it must run in a background thread while your main thread sends requests.
Every request is asynchronous. connect() returns before the handshake finishes, and nextValidId() is the first reliable sign that the session is usable. Requests sent earlier can be lost, which is a classic race condition.
Data arrives one callback per row. position() fires once per account and contract, and positionEnd() is the only signal that the set is complete. A threading.Event with a timeout is the right wait, because it returns the moment the callback fires and cannot hang forever. time.sleep(2) either wastes time or reads half the data.
Collect rows in a plain list of dicts and call pd.DataFrame(rows) once. Appending to a DataFrame row by row copies the whole frame each time, which is slow, and forces you to invent a unique index such as account plus symbol. A list append is cheap, and the DataFrame gets a clean default index.
Which call should I use: reqPositions, reqAccountSummary, reqAccountUpdates or reqPnL?
Each call answers a different question, and picking the wrong one is the main source of confusing multi-account behaviour.
| Need | Call | Completion signal | Notes |
|---|---|---|---|
| Holdings and average cost, all accounts | reqPositions() | positionEnd() | Non-zero positions only; no market value or P&L |
| NAV, cash, buying power, all accounts | reqAccountSummary(reqId, 'All', tags) | accountSummaryEnd() | Cancel with cancelAccountSummary when done |
| Market value, unrealized P&L per position, one account | reqAccountUpdates(True, acct) | accountDownloadEnd() | Only one account subscribed at a time; unsubscribe before switching |
| Live daily, unrealized and realized P&L | reqPnL / reqPnLSingle | Streaming (no end) | Needs the account code; reqPnLSingle also needs the conId; cancel reqPnL with cancelPnL and reqPnLSingle with cancelPnLSingle |
How do I handle multiple accounts, average cost and missing positions?
reqPositions() returns every account the login can see, including linked and advisor sub-accounts, each tagged in the account field. Use groupby to split them:
pos.groupby('account').size() counts holdings per account, and pos[pos.account == 'DU123456'] isolates one. Advisor (FA) logins also see allocation groups, but the position rows still carry individual account codes.
The pos sign is the direction: positive is long, negative is short. A closed position is not reported as zero. It simply disappears, and the list contains only non-zero holdings.
avgCost is per share for stocks, but for futures and options it includes the multiplier. An option showing avgCost 250 with multiplier 100 means a premium of 2.50 per unit. Normalise it:
pos["mult"] = pd.to_numeric(pos["multiplier"], errors="coerce").fillna(1)
pos["unit_cost"] = pos["avgCost"] / pos["mult"]
pos["cost_basis"] = pos["unit_cost"] * pos["pos"] * pos["mult"]Are errors 2104, 2106 and 2158 actually failures?
No. They arrive through the same error() callback, but they are status notices. The wrapper uses one method for both, so an unfiltered handler prints noise that looks alarming.
Override error() and drop the known informational codes, as in the script above. If you get TypeError: error() missing or takes N positional arguments, your signature does not match the installed ibapi. Recent versions add advancedOrderRejectJson, so give it a default value and add *extra to stay tolerant.
| Code | Meaning | Action |
|---|---|---|
| 2104 | Market data farm connection is OK | Ignore |
| 2106 | HMDS data farm connection is OK | Ignore |
| 2107 | HMDS data farm connection inactive but available on demand | Ignore |
| 2119 | Market data farm is connecting | Ignore |
| 2158 | Sec-def data farm connection is OK | Ignore |
Why does my script fail, hang or return nothing?
Most failures come from the connection layer or from waiting on a callback that never fires. Match the exact text to the cause.
| Symptom or text | Cause | Fix |
|---|---|---|
| Error 502 / ConnectionRefusedError | API socket not enabled, app not running, or wrong port | Enable socket clients; match port to the table above |
| Error 326: client id is already in use | Another script or TWS session holds that clientId | Use a different clientId or disconnect the stale process |
| Script hangs forever | End callback never fired, or main thread waits without timeout | Use Event.wait(timeout); check the error output |
| Empty DataFrame | Account has no open positions, or data read before positionEnd | Wait on the Event; confirm the right account and mode (paper vs live) |
| Requests silently ignored | Sent before nextValidId | Wait on the ready Event first |
| Error 504: Not connected | Request sent after disconnect or failed connect | Check connect result and handle reconnects explicitly |
How does ib_insync or ib_async compare with ibapi?
ib_insync wraps the same socket protocol in a synchronous, event-loop-based interface, so there are no callbacks or manual events. It is no longer maintained, and the community fork ib_async (pip install ib_async) is the actively developed continuation with the same API style.
The ibapi script above shrinks to a few lines:
from ib_async import IB, util # or: from ib_insync import IB, util
import pandas as pd
ib = IB()
ib.connect("127.0.0.1", 7497, clientId=18, readonly=True)
pos = pd.DataFrame([{"account": p.account, "symbol": p.contract.symbol,
"secType": p.contract.secType, "pos": p.position,
"avgCost": p.avgCost} for p in ib.positions()])
summ = util.df(ib.accountSummary())
print(pos, summ[summ.tag == "NetLiquidation"])
ib.disconnect() Common questions
Does reqPositions return positions for all linked or advisor accounts?
Yes. It returns positions for every account the logged-in user can access, and each position() call carries the account code. Filter or group on that field afterwards.
Why is my ibapi error handler raising a TypeError?
Your error() signature has a different number of arguments from what the installed ibapi passes. Recent versions add advancedOrderRejectJson after errorString, so declare it with a default and add *extra to tolerate further additions.
Can I use reqAccountUpdates to get NAV for several accounts?
Not at once. reqAccountUpdates allows only one account subscription at a time, so you would have to subscribe, wait for accountDownloadEnd, unsubscribe and repeat. reqAccountSummary with the 'All' group is simpler for NAV across accounts.
How do I get positions only once instead of streaming updates?
Call reqPositions(), wait for positionEnd(), then call cancelPositions(). Without the cancel, IB keeps sending position changes to your wrapper until you disconnect.
Is it safe to leave the API on a live port for a read-only script?
Tick Read-Only API in the settings so order requests are rejected, restrict Trusted IPs to localhost, and use a dedicated clientId. This limits the damage from a bug or a copied order sample.