Howwise RSS
Trading Software7 min read

Interactive Brokers API: Get Positions in Python (ibapi)

Short answer

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.

Key takeaways
  • 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"))
Tip: Account summary values arrive as strings. Convert with pd.to_numeric(summ['value'], errors='coerce') before doing arithmetic, and keep the currency column, because a multi-currency account reports per-currency rows for some tags.

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.

ApplicationModeDefault socket port
TWSLive7496
TWSPaper7497
IB GatewayLive4001
IB GatewayPaper4002
  1. In TWS or Gateway open Global Configuration > API > Settings.
  2. Tick Enable ActiveX and Socket Clients.
  3. Confirm the Socket port matches your connect() call.
  4. Tick Read-Only API if the script only reads data. Order requests are then rejected, which protects against accidental trades.
  5. Add 127.0.0.1 to Trusted IPs to avoid the confirmation popup, and pick a unique clientId per script.
Warning: Develop against a paper account. A script with Read-Only API unticked and a copy-pasted order sample can send real orders on a live port.

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.

NeedCallCompletion signalNotes
Holdings and average cost, all accountsreqPositions()positionEnd()Non-zero positions only; no market value or P&L
NAV, cash, buying power, all accountsreqAccountSummary(reqId, 'All', tags)accountSummaryEnd()Cancel with cancelAccountSummary when done
Market value, unrealized P&L per position, one accountreqAccountUpdates(True, acct)accountDownloadEnd()Only one account subscribed at a time; unsubscribe before switching
Live daily, unrealized and realized P&LreqPnL / reqPnLSingleStreaming (no end)Needs the account code; reqPnLSingle also needs the conId; cancel reqPnL with cancelPnL and reqPnLSingle with cancelPnLSingle
Tip: Valid reqAccountSummary tags include NetLiquidation, TotalCashValue, BuyingPower, AvailableFunds, GrossPositionValue and MaintMarginReq. Passing the group 'All' returns every account the login can access.

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"]
Tip: The Contract in position() is thin. For expiry, strike, trading class or full exchange, call reqContractDetails with a Contract built from the conId and wait for contractDetailsEnd. Mind the pacing limit of roughly 50 messages per second when looping over many positions.

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.

CodeMeaningAction
2104Market data farm connection is OKIgnore
2106HMDS data farm connection is OKIgnore
2107HMDS data farm connection inactive but available on demandIgnore
2119Market data farm is connectingIgnore
2158Sec-def data farm connection is OKIgnore

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 textCauseFix
Error 502 / ConnectionRefusedErrorAPI socket not enabled, app not running, or wrong portEnable socket clients; match port to the table above
Error 326: client id is already in useAnother script or TWS session holds that clientIdUse a different clientId or disconnect the stale process
Script hangs foreverEnd callback never fired, or main thread waits without timeoutUse Event.wait(timeout); check the error output
Empty DataFrameAccount has no open positions, or data read before positionEndWait on the Event; confirm the right account and mode (paper vs live)
Requests silently ignoredSent before nextValidIdWait on the ready Event first
Error 504: Not connectedRequest sent after disconnect or failed connectCheck connect result and handle reconnects explicitly
Warning: A TWS login session can be dropped by the daily restart or by a second login elsewhere. A long-running script needs a reconnect path that waits for nextValidId again and re-issues its requests.

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.

Related topics:interactive brokersibapipythonpandasibasync