A CLI tool for analyzing Interactive Brokers (IBKR) holdings and trades. Downloads data via the IBKR Flex Query API, computes FIFO tax lots, and displays holdings with prices, positions, and USD conversions. Supports multiple IBKR accounts.
- An Interactive Brokers account
- Go 1.25+
# Create an ibctl directory.
mkdir ~/ibkr && cd ~/ibkr
# Initialize configuration.
ibctl config init
# Edit the config to add your Flex Query ID and account aliases.
ibctl config edit
# Set the IBKR token.
export IBKR_FLEX_WEB_SERVICE_TOKEN="your-flex-web-service-token"
# View holdings (downloads data automatically).
ibctl holding listAll commands operate on an ibctl directory specified by --dir (defaults to the current directory). This directory has a well-known layout:
<dir>/
├── ibctl.yaml # Configuration file
├── data/ # Persistent — do not delete
│ └── accounts/<alias>/
│ └── trades.json # Incrementally merged trade history
├── cache/ # Safe to delete — re-populated on next download
│ ├── accounts/<alias>/
│ │ ├── positions.json # Latest IBKR-reported positions snapshot
│ │ ├── transfers.json # Position transfers (ACATS, FOP, internal)
│ │ ├── trade_transfers.json # Cost basis for transferred positions
│ │ ├── corporate_actions.json # Stock splits, mergers, spinoffs
│ │ └── cash_positions.json # Cash balances by currency
│ └── fx/<BASE>.<QUOTE>/
│ └── rates.json # Daily FX rates per currency pair
├── activity_statements/ # User-managed IBKR Activity Statement CSVs
│ └── <alias>/*.csv
└── seed/ # Optional — pre-transfer tax lots from previous brokers
└── <alias>/transactions.json
data/containstrades.jsonper account, incrementally merged across downloads. This is the only directory that accumulates over time — IBKR limits each download to 365 days, so older trades can't be re-downloaded.cache/contains everything else: position snapshots, transfers, FX rates. Safe to delete entirely — the nextibctl downloadre-populates it.activity_statements/contains Activity Statement CSVs you download from the IBKR portal. ibctl reads them at command time and never modifies them.seed/(optional) contains permanent transaction history imported from previous brokers (e.g., UBS, RBC).
Follow these steps in the IBKR portal to create a Flex Query and generate an API token.
- Log in to IBKR Account Management.
- Navigate to Performance & Reports > Flex Queries.
- In the Activity Flex Query section, click the + button.
- Set the Query Name to something descriptive (e.g., "ibctl").
- Under Sections, add the following sections, selecting all fields for each:
- Trades
- Open Positions
- Cash Transactions (used for FX rate extraction)
- Cash Report (provides cash balances by currency)
- Transfers (ACATS, Internal) (captures positions transferred from other brokers)
- Incoming/Outgoing Trade Transfers (preserves cost basis and holding period)
- Corporate Actions (captures stock splits, mergers, spinoffs)
- Under Delivery Configuration, set:
- Format:
XML - Period:
Last 365 Calendar Days
- Format:
- Under General Configuration, set:
- Date Format:
yyyyMMdd - Time Format:
HHmmss - Date/Time Separator:
; (semi-colon) - Include Canceled Trades?:
No - Include Currency Rates?:
Yes - Include Audit Trail Fields?:
No - Breakout by Day?:
No
- Date Format:
- Click Save.
- Note the Query ID displayed next to the query name.
Note on trade history: IBKR limits Flex Query periods to 365 calendar days. To capture older trades, change the Period in the IBKR portal to cover a different date range and run ibctl download again — new trades are merged into the existing cache, deduplicated by trade ID.
- On the same Flex Queries page, find the Flex Web Service Configuration section.
- Click the gear icon to configure.
- Generate a token and copy it securely.
- Set it as the
IBKR_FLEX_WEB_SERVICE_TOKENenvironment variable.
| Variable | Required | Description |
|---|---|---|
IBKR_FLEX_WEB_SERVICE_TOKEN |
Yes (for download) |
IBKR Flex Web Service token. Read-only — can only retrieve reports, not make trades. Never store in config files or version control. |
The ibctl.yaml file lives in the ibctl directory:
version: v1
flex_query_id: "123456"
accounts:
rrsp: "U1234567"
holdco: "U2345678"
individual: "U3456789"
categorization:
- symbol: AAPL
category: EQUITY
type: STOCK
sector: TECH
geo: US
cash:
CAD: "-5000.00"
additions:
- account_alias: wealthsimple
symbol: VFV.TO
currency_code: CAD
trade_date: "20240315"
trade_price: "102.50"
trade_side: buy
quantity: "100"
last_price: "115.30"
tax_exempt: trueflex_query_id— your IBKR Flex Query ID (required)accounts— maps user-chosen aliases to IBKR account IDs (required). Account numbers are confidential — only aliases appear in output and directory names.categorization— optional classification metadata for holdings display (category, type, sector, geo)cash— optional manual cash adjustments by currency code (applied to cash positions in holdings display)additions— optional manually added trades from non-IBKR brokers. Addition accounts must not match IBKR account aliases. Trade dates use YYYYMMDD format. Whentax_exemptis true, lots show P&L=0 and are excluded from tax calculations.
# Set the IBKR token.
export IBKR_FLEX_WEB_SERVICE_TOKEN="your-flex-web-service-token"
# View combined holding list (downloads data automatically).
ibctl holding list
ibctl holding list --format csv
ibctl holding list --format json
ibctl holding list --cached # Skip download, use cached data only
# Force re-download of IBKR data (all accounts).
ibctl download
# Probe the API to see what data is available per account.
ibctl probe
# Archive the ibctl directory to a zip file.
ibctl data zip -o backup.zip
# Use a different ibctl directory (default is current directory).
ibctl holding list --dir ~/Documents/ibkr| Command | Description |
|---|---|
ibctl config init |
Create a new ibctl.yaml in the ibctl directory |
ibctl config edit |
Edit ibctl.yaml in $EDITOR |
ibctl config validate |
Validate ibctl.yaml |
ibctl data zip -o <file> |
Archive the ibctl directory to a zip file |
ibctl download |
Download and cache IBKR data via Flex Query API |
ibctl holding list |
Display holdings with prices, positions, and classifications |
ibctl probe |
Probe the API and show per-account data counts |
All commands accept --dir to specify the ibctl directory (defaults to .).
IBKR limits all data access to 365 days per request. To get your full trade history, download Activity Statement CSVs from the IBKR portal.
-
Create subdirectories for each account using your aliases:
mkdir -p activity_statements/rrsp mkdir -p activity_statements/holdco mkdir -p activity_statements/individual
-
For each account, log in to IBKR Account Management.
-
Go to Performance & Reports > Statements.
-
Select Activity Statement, Custom Date Range, and click Download CSV.
-
Download yearly chunks (365-day maximum per file).
-
Save each CSV in the account's subdirectory. Filenames don't matter — ibctl reads all
*.csvfiles recursively. -
Run
ibctl holding list— data from the CSVs is merged with Flex Query API data.
At command time, ibctl merges three data sources per account. CSV data takes precedence for overlapping date ranges:
- Activity Statement CSVs (
activity_statements/<alias>/*.csv) — primary source of truth for trades - Seed data (
seed/<alias>/transactions.json) — imported transactions from previous brokers - Flex Query cache (
data/accounts/<alias>/trades.json) — recent trades from the API, used only for dates not covered by CSVs
All data files use newline-separated protobuf JSON (one message per line). Monetary values use standard.money.v1.Money (units + micros for 6 decimal places). Dates use standard.time.v1.Date (year, month, day).
| File | Proto Message | Merge Strategy | Purpose |
|---|---|---|---|
trades.json |
ibctl.data.v1.Trade |
Deduplicated by trade ID | Persistent trade history. Incrementally merged across downloads so the cache grows over time. |
positions.json |
ibctl.data.v1.Position |
Overwritten each download | IBKR-reported positions snapshot. Provides current market prices and verification data. Not the source of truth for quantities or cost basis — those are computed via FIFO from trades. |
transfers.json |
ibctl.data.v1.Transfer |
Overwritten each download | Position transfers (ACATS, ATON, FOP, internal). Transfer-ins with a non-zero price become synthetic buy trades for FIFO. |
trade_transfers.json |
ibctl.data.v1.TradeTransfer |
Overwritten each download | Preserves original trade date and cost basis for transferred positions (long-term vs short-term capital gains). |
corporate_actions.json |
ibctl.data.v1.CorporateAction |
Overwritten each download | Stock splits, mergers, spinoffs for audit purposes. |
cash_positions.json |
ibctl.data.v1.CashPosition |
Overwritten each download | Cash balances by currency from the IBKR Cash Report section. |
rates.json |
ibctl.data.v1.ExchangeRate |
Deduplicated by date | Per-pair FX rates from Bank of Canada (X→CAD) and frankfurter.dev (X→USD). Only missing dates are fetched. |
The optional seed/ directory contains permanent, manually curated transaction history from previous brokers. transactions.json uses the ibctl.data.v1.ImportedTransaction proto covering all transaction types (buys, sells, splits, dividends, interest, fees, etc.). Only security-affecting transactions are converted to Trade protos for FIFO processing.
The holding list command runs:
- Download: Fetches all accounts' data from the IBKR Flex Query API. Trades are incrementally merged. FX rates are eagerly downloaded for all currency pairs from the earliest trade date to today.
- Merge: Combines Activity Statement CSVs + seed data + Flex Query cache, with CSV data taking precedence for overlapping dates.
- FIFO: Computes tax lots grouped by (account, symbol). Transfers and trade transfers are converted to synthetic trades. Buys before sells within the same date.
- Aggregation: Tax lots are aggregated into positions with weighted average cost basis, then combined across accounts.
- Verification: Computed positions are compared against IBKR-reported positions. Cost basis discrepancies > 0.1% are logged as warnings.
- Display: Holdings are rendered with USD conversions (via FX rates), market value, unrealized P&L split into short-term and long-term capital gains, and optional symbol classifications.