Skip to content

Commit b4ec39d

Browse files
committed
commit
1 parent 7fa45bf commit b4ec39d

File tree

40 files changed

+2397
-664
lines changed

40 files changed

+2397
-664
lines changed

README.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ibctl
22

3-
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/CAD conversions. Supports multiple IBKR accounts, non-IBKR broker additions, and tax reporting for both IRS and CRA.
3+
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 multi-currency conversions. Supports multiple IBKR accounts, non-IBKR broker additions, and tax reporting for both IRS and CRA.
44

55
For complete documentation, run `make book` to view the ibctl book locally, or browse the `book/` directory.
66

@@ -26,6 +26,12 @@ export IBKR_FLEX_WEB_SERVICE_TOKEN="your-flex-web-service-token"
2626

2727
# View holdings (downloads data automatically).
2828
ibctl holding list
29+
30+
# View holdings in CAD instead of USD.
31+
ibctl holding list --base-currency CAD
32+
33+
# View all transactions for the year.
34+
ibctl transaction list --from 20250101 --to 20251231 --base-currency USD
2935
```
3036

3137
## Environment Variables
@@ -48,21 +54,28 @@ ibctl holding list
4854
| `ibctl holding category list` | Display holdings aggregated by category |
4955
| `ibctl holding geo list` | Display holdings aggregated by geographic classification |
5056
| `ibctl holding value` | Display portfolio value with estimated tax impact |
51-
| `ibctl trade sale list` | List realized security sales with FIFO lot matching for tax reporting |
57+
| `ibctl transaction list` | List all transactions (buys, sells, dividends, interest, WHT, etc.) chronologically |
58+
| `ibctl transaction sale list` | List realized security sales with FIFO lot matching for tax reporting |
5259
| `ibctl probe` | Probe the API and show per-account data counts |
5360

54-
All commands accept `--dir` to specify the ibctl directory (defaults to `.`). Use `--help` on any command for detailed documentation.
61+
All commands accept `--dir` to specify the ibctl directory (defaults to `.`). All holding and transaction commands accept `--base-currency` (default `USD`) to convert values to a different currency. Use `--help` on any command for detailed documentation.
5562

5663
## Tax Reporting
5764

5865
Generate CSV files for your accountant:
5966

6067
```bash
61-
# For IRS (US tax reporting).
62-
ibctl trade sale list --from 20250101 --to 20251231 --base-currency USD --format csv > trades-2025-usd.csv
68+
# Realized capital gains for IRS (US tax reporting).
69+
ibctl transaction sale list --from 20250101 --to 20251231 --base-currency USD --format csv > transaction-sale-list-2025-usd.csv
70+
71+
# Realized capital gains for CRA (Canadian tax reporting).
72+
ibctl transaction sale list --from 20250101 --to 20251231 --base-currency CAD --format csv > transaction-sale-list-2025-cad.csv
73+
74+
# Complete transaction history for IRS (dividends, interest, WHT, buys, sells, etc.).
75+
ibctl transaction list --from 20250101 --to 20251231 --base-currency USD --format csv > transaction-list-2025-usd.csv
6376

64-
# For CRA (Canadian tax reporting).
65-
ibctl trade sale list --from 20250101 --to 20251231 --base-currency CAD --format csv > trades-2025-cad.csv
77+
# Complete transaction history for CRA.
78+
ibctl transaction list --from 20250101 --to 20251231 --base-currency CAD --format csv > transaction-list-2025-cad.csv
6679
```
6780

6881
See the [Tax Reporting Guide](book/src/tax-reporting.md) in the ibctl book for a detailed explanation of the CSV columns and methodology, suitable for sharing with your accountant.

book/src/SUMMARY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [holding category list](commands/holding-category-list.md)
1818
- [holding geo list](commands/holding-geo-list.md)
1919
- [holding value](commands/holding-value.md)
20-
- [trade sale list](commands/trade-sale-list.md)
20+
- [transaction list](commands/transaction-list.md)
21+
- [transaction sale list](commands/transaction-sale-list.md)
2122
- [Tax Reporting Guide](tax-reporting.md)
2223
- [Glossary](glossary.md)

book/src/commands/download.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Fetches IBKR data via the Flex Query API and stores it locally. Also eagerly dow
1010

1111
The download command makes a single Flex Query API call using the `flex_query_id` from `ibctl.yaml` and the `IBKR_FLEX_WEB_SERVICE_TOKEN` environment variable. The response is split per account (matched by account ID to your configured aliases) and written to two locations:
1212

13-
- **`data/`** -- Trades are written here. This directory is persistent and append-only. New trades are merged with existing trades, deduplicated by trade ID. This ensures trade history accumulates over time even if your Flex Query period is limited.
13+
- **`data/`** -- Trades and income are written here. This directory is persistent and append-only. New trades are merged with existing trades, deduplicated by trade ID. Income records (dividends, interest, withholding tax) from Flex Query CashTransactions are converted to Income protos and persisted alongside trades. This ensures trade and income history accumulates over time even if your Flex Query period is limited.
1414
- **`cache/`** -- Positions, cash positions, transfers, trade transfers, and corporate actions are written here. These are point-in-time snapshots that are overwritten on each download.
1515

1616
After writing account data, the downloader collects all non-USD currency codes from trades and eagerly downloads FX rates for each currency pair. Rates are stored in `cache/fx/{BASE}.{QUOTE}/rates.json`. Sources:
@@ -20,9 +20,24 @@ After writing account data, the downloader collects all non-USD currency codes f
2020

2121
Weekend and holiday dates produce no-data markers in the rate files. When a rate is needed for such a date, ibctl falls back to the most recent business day rate.
2222

23+
## Option exercises, assignments, and expirations
24+
25+
Option Exercises, Assignments and Expirations from the Flex Query are converted to synthetic trades during download:
26+
27+
- **Expirations** become sell trades at $0 (creating a loss for option buyers, a gain for writers).
28+
- **Exercises and assignments** create synthetic stock trades at the strike price.
29+
30+
These synthetic trades are merged into the trade data alongside regular trades and flow through FIFO like any other trade.
31+
32+
## Income data persistence
33+
34+
Flex Query CashTransactions are parsed for dividends, interest, and withholding tax. These are converted to Income protos (with an `IncomeType` enum: DIVIDEND, INTEREST, WITHHOLDING_TAX) and persisted in `data/accounts/<alias>/income.json`. This ensures income data accumulates over time, just like trade data.
35+
36+
Activity Statement CSV dividends, interest, and withholding tax are read at command time (not during download) and merged with persisted income data for display in the `transaction list` output.
37+
2338
## Idempotency
2439

25-
The download command is idempotent. Running it multiple times safely merges new trades into the existing data directory without creating duplicates. Cache data is simply overwritten with the latest snapshot.
40+
The download command is idempotent. Running it multiple times safely merges new trades and income into the existing data directory without creating duplicates. Cache data is simply overwritten with the latest snapshot.
2641

2742
## Flags
2843

@@ -34,4 +49,3 @@ The download command is idempotent. Running it multiple times safely merges new
3449

3550
- The `--download` flag on other commands (such as `ibctl holding list --download`) calls this same download logic before displaying results.
3651
- Unknown account IDs in the Flex Query response are logged as warnings and skipped. Add them to the `accounts` section of `ibctl.yaml` to process them.
37-
- Option exercises, assignments, and expirations from the Flex Query are converted to synthetic trades and merged alongside regular trades.
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
# holding category list
22

33
```
4-
ibctl holding category list [--dir DIR] [--format FORMAT] [--download] [--geo GEO]
4+
ibctl holding category list [--dir DIR] [--format FORMAT] [--download] [--geo GEO] [--base-currency CURRENCY]
55
```
66

77
Aggregates holdings by category and displays each category's market value, percentage of net liquidation value, and capital gains breakdown.
88

99
Categories are defined in the `categorization` section of `ibctl.yaml`. Cash positions default to category `CASH`. Symbols without a configured category appear as `UNCATEGORIZED`.
1010

11+
Use `--base-currency` (default `USD`) to convert all values to a different currency.
12+
1113
## Columns
1214

1315
| Column | Description |
1416
|--------|-------------|
1517
| CATEGORY | Asset category (e.g., `EQUITY`, `FIXED_INCOME`, `CASH`, `UNCATEGORIZED`) |
16-
| MKT VAL USD | Total market value in USD for all holdings in this category |
18+
| MKT VAL {BASE} | Total market value in base currency for all holdings in this category |
1719
| NET LIQ % | Percentage of total portfolio value represented by this category |
18-
| P&L USD | Total unrealized P&L in USD |
19-
| STCG USD | Total short-term unrealized capital gain in USD |
20-
| LTCG USD | Total long-term unrealized capital gain in USD |
20+
| P&L {BASE} | Total unrealized P&L in base currency |
21+
| STCG {BASE} | Total short-term unrealized capital gain in base currency |
22+
| LTCG {BASE} | Total long-term unrealized capital gain in base currency |
23+
24+
`{BASE}` is replaced with the `--base-currency` value (e.g., `MKT VAL USD`, `P&L CAD`).
2125

2226
## Sort order
2327

24-
Categories are sorted by market value in USD descending (highest value first).
28+
Categories are sorted by market value in base currency descending (highest value first).
2529

2630
## Filtering by geo
2731

@@ -35,3 +39,4 @@ Use `--geo` to filter holdings to a single geographic classification before aggr
3539
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
3640
| `--download` | `false` | Download fresh data before displaying |
3741
| `--geo` | (all) | Filter by geographic classification before aggregating (e.g., `US`, `INTL`) |
42+
| `--base-currency` | `USD` | Base currency for value conversion (case-insensitive, e.g., `USD`, `CAD`) |
Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
# holding geo list
22

33
```
4-
ibctl holding geo list [--dir DIR] [--format FORMAT] [--download] [--category CATEGORY]
4+
ibctl holding geo list [--dir DIR] [--format FORMAT] [--download] [--category CATEGORY] [--base-currency CURRENCY]
55
```
66

77
Aggregates holdings by geographic classification and displays each geo's market value, percentage of net liquidation value, and capital gains breakdown.
88

99
Geographic classifications are defined in the `categorization` section of `ibctl.yaml`. Symbols without a configured geo appear as `UNCATEGORIZED`.
1010

11+
Use `--base-currency` (default `USD`) to convert all values to a different currency.
12+
1113
## Columns
1214

1315
| Column | Description |
1416
|--------|-------------|
1517
| GEO | Geographic classification (e.g., `US`, `INTL`, `CA`, `UNCATEGORIZED`) |
16-
| MKT VAL USD | Total market value in USD for all holdings in this geo |
18+
| MKT VAL {BASE} | Total market value in base currency for all holdings in this geo |
1719
| NET LIQ % | Percentage of total portfolio value represented by this geo |
18-
| P&L USD | Total unrealized P&L in USD |
19-
| STCG USD | Total short-term unrealized capital gain in USD |
20-
| LTCG USD | Total long-term unrealized capital gain in USD |
20+
| P&L {BASE} | Total unrealized P&L in base currency |
21+
| STCG {BASE} | Total short-term unrealized capital gain in base currency |
22+
| LTCG {BASE} | Total long-term unrealized capital gain in base currency |
23+
24+
`{BASE}` is replaced with the `--base-currency` value (e.g., `MKT VAL USD`, `P&L CAD`).
2125

2226
## Sort order
2327

24-
Geos are sorted by market value in USD descending (highest value first).
28+
Geos are sorted by market value in base currency descending (highest value first).
2529

2630
## Filtering by category
2731

@@ -35,3 +39,4 @@ Use `--category` to filter holdings to a single category before aggregating by g
3539
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
3640
| `--download` | `false` | Download fresh data before displaying |
3741
| `--category` | (all) | Filter by category before aggregating (e.g., `EQUITY`, `FIXED_INCOME`) |
42+
| `--base-currency` | `USD` | Base currency for value conversion (case-insensitive, e.g., `USD`, `CAD`) |

book/src/commands/holding-list.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
# holding list
22

33
```
4-
ibctl holding list [--dir DIR] [--format FORMAT] [--download]
4+
ibctl holding list [--dir DIR] [--format FORMAT] [--download] [--base-currency CURRENCY]
55
```
66

77
Shows combined positions across all accounts. Positions are computed via FIFO tax lot matching with weighted average cost basis, then verified against IBKR-reported positions.
88

9+
Use `--base-currency` (default `USD`) to convert all values to a different currency. Cost basis is converted using the FX rate on the lot's open date; current market value uses today's rate. This correctly shows FX-adjusted unrealized P&L -- the same methodology used by `transaction sale list` for realized gains.
10+
11+
Positions worth less than 0.01 in the base currency are filtered out. This removes dust positions (e.g., a JPY cash balance that rounds to -0.00 in USD).
12+
913
## Columns
1014

1115
| Column | Description |
@@ -14,21 +18,23 @@ Shows combined positions across all accounts. Positions are computed via FIFO ta
1418
| CURRENCY | Native currency of the last price and average price |
1519
| LAST PRICE | Most recent market price per share in native currency |
1620
| AVG PRICE | Weighted average cost basis per share in native currency |
17-
| LAST USD | Last price converted to USD using the most recent FX rate |
18-
| AVG USD | Average cost basis converted to USD |
19-
| MKT VAL USD | Market value in USD (last price USD * position) |
20-
| P&L USD | Unrealized profit/loss in USD (computed per lot, excluding tax-exempt lots) |
21-
| STCG USD | Short-term unrealized capital gain in USD (lots held < 365 days) |
22-
| LTCG USD | Long-term unrealized capital gain in USD (lots held >= 365 days) |
21+
| LAST {BASE} | Last price converted to base currency using today's FX rate |
22+
| AVG {BASE} | Average cost basis converted to base currency using date-specific FX rates from each lot's open date |
23+
| MKT VAL {BASE} | Market value in base currency (last price base * position) |
24+
| P&L {BASE} | Unrealized profit/loss in base currency (computed per lot, excluding tax-exempt lots, using date-specific FX rates) |
25+
| STCG {BASE} | Short-term unrealized capital gain in base currency (lots held < 365 days) |
26+
| LTCG {BASE} | Long-term unrealized capital gain in base currency (lots held >= 365 days) |
2327
| POSITION | Total quantity held across all accounts |
2428
| CATEGORY | User-defined asset category from configuration (e.g., `EQUITY`, `FIXED_INCOME`) |
2529
| TYPE | User-defined asset type (e.g., `STOCK`, `ETF`) |
2630
| SECTOR | User-defined sector classification (e.g., `TECH`) |
2731
| GEO | User-defined geographic classification (e.g., `US`, `INTL`) |
2832

33+
`{BASE}` is replaced with the `--base-currency` value (e.g., `LAST USD`, `MKT VAL CAD`).
34+
2935
## Sort order
3036

31-
Securities are sorted by market value in USD descending (highest value first). Cash positions appear at the bottom, also sorted by market value descending.
37+
Securities are sorted by market value in base currency descending (highest value first). Cash positions appear at the bottom, also sorted by market value descending.
3238

3339
## Cash positions
3440

@@ -44,7 +50,7 @@ Holdings from tax-exempt accounts (RRSP, TFSA, or additions with `tax_exempt: tr
4450

4551
## Totals row
4652

47-
The table output includes a totals row summing MKT VAL USD, P&L USD, STCG USD, and LTCG USD.
53+
The table output includes a totals row summing MKT VAL {BASE}, P&L {BASE}, STCG {BASE}, and LTCG {BASE}.
4854

4955
## Discrepancy warnings
5056

@@ -63,3 +69,4 @@ Unmatched sells (where the buy occurred before the data window) are also logged
6369
| `--dir` | `.` | The ibctl directory containing `ibctl.yaml` |
6470
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
6571
| `--download` | `false` | Download fresh data before displaying |
72+
| `--base-currency` | `USD` | Base currency for value conversion (case-insensitive, e.g., `USD`, `CAD`) |
Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# holding lot list
22

33
```
4-
ibctl holding lot list [--dir DIR] [--format FORMAT] [--download] [--symbol SYMBOL]
4+
ibctl holding lot list [--dir DIR] [--format FORMAT] [--download] [--symbol SYMBOL] [--base-currency CURRENCY]
55
```
66

77
Lists individual FIFO tax lots. Unlike `holding list`, which aggregates positions by symbol, this command shows each tax lot separately with its own open date, quantity, and cost basis.
88

9+
Use `--base-currency` (default `USD`) to convert all values to a different currency. Cost basis is converted using the FX rate on the lot's open date; current market value uses today's rate. This means each lot's base-currency cost basis reflects the FX rate at the time of purchase, producing accurate FX-adjusted unrealized P&L.
10+
911
## Columns
1012

1113
| Column | Description |
@@ -18,27 +20,29 @@ Lists individual FIFO tax lots. Unlike `holding list`, which aggregates position
1820
| AVG PRICE | Cost basis price per share in native currency |
1921
| P&L | Unrealized P&L in native currency |
2022
| MKT VAL | Current market value in native currency |
21-
| AVG USD | Cost basis price converted to USD |
22-
| P&L USD | Unrealized P&L in USD |
23-
| STCG USD | Short-term unrealized P&L in USD (lot held < 365 days), equals P&L USD or 0 |
24-
| LTCG USD | Long-term unrealized P&L in USD (lot held >= 365 days), equals P&L USD or 0 |
25-
| MKT VAL USD | Current market value in USD |
23+
| AVG {BASE} | Cost basis price converted to base currency using the FX rate on the lot's open date |
24+
| P&L {BASE} | Unrealized P&L in base currency |
25+
| STCG {BASE} | Short-term unrealized P&L in base currency (lot held < 365 days), equals P&L {BASE} or 0 |
26+
| LTCG {BASE} | Long-term unrealized P&L in base currency (lot held >= 365 days), equals P&L {BASE} or 0 |
27+
| MKT VAL {BASE} | Current market value in base currency |
2628
| CATEGORY | User-defined asset category from configuration |
2729
| TYPE | User-defined asset type |
2830
| SECTOR | User-defined sector classification |
2931
| GEO | User-defined geographic classification |
3032

33+
`{BASE}` is replaced with the `--base-currency` value (e.g., `AVG USD`, `P&L CAD`).
34+
3135
## Sort order
3236

3337
Lots are sorted by date (oldest first), then by symbol, then by account.
3438

3539
## Tax-exempt lots
3640

37-
Lots from tax-exempt accounts show zero P&L, P&L USD, STCG USD, and LTCG USD.
41+
Lots from tax-exempt accounts show zero P&L, P&L {BASE}, STCG {BASE}, and LTCG {BASE}.
3842

3943
## Totals row
4044

41-
The table output includes a totals row summing P&L USD, STCG USD, LTCG USD, and MKT VAL USD.
45+
The table output includes a totals row summing P&L {BASE}, STCG {BASE}, LTCG {BASE}, and MKT VAL {BASE}.
4246

4347
## Flags
4448

@@ -48,3 +52,4 @@ The table output includes a totals row summing P&L USD, STCG USD, LTCG USD, and
4852
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
4953
| `--download` | `false` | Download fresh data before displaying |
5054
| `--symbol` | (all) | Filter to a specific symbol. Omit to show all symbols. |
55+
| `--base-currency` | `USD` | Base currency for value conversion (case-insensitive, e.g., `USD`, `CAD`) |

book/src/commands/holding-value.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# holding value
22

33
```
4-
ibctl holding value [--dir DIR] [--download]
4+
ibctl holding value [--dir DIR] [--download] [--base-currency CURRENCY]
55
```
66

77
Displays the total portfolio value, unrealized short-term and long-term capital gains, estimated tax liability, and after-tax portfolio value.
88

9+
Use `--base-currency` (default `USD`) to view the summary in a different currency.
10+
911
## Output
1012

1113
The command prints a summary to stdout:
@@ -36,9 +38,9 @@ The formulas are:
3638
3739
| Value | Formula |
3840
|-------|---------|
39-
| Portfolio Value | Sum of MKT VAL USD across all holdings |
40-
| STCG | Sum of STCG USD across all holdings |
41-
| LTCG | Sum of LTCG USD across all holdings |
41+
| Portfolio Value | Sum of MKT VAL {BASE} across all holdings |
42+
| STCG | Sum of STCG {BASE} across all holdings |
43+
| LTCG | Sum of LTCG {BASE} across all holdings |
4244
| STCG Tax | STCG * `taxes.stcg` rate |
4345
| LTCG Tax | LTCG * `taxes.ltcg` rate |
4446
| Total Tax | STCG Tax + LTCG Tax |
@@ -52,3 +54,4 @@ Tax-exempt holdings (RRSP, TFSA, additions with `tax_exempt: true`) contribute z
5254
|------|---------|-------------|
5355
| `--dir` | `.` | The ibctl directory containing `ibctl.yaml` |
5456
| `--download` | `false` | Download fresh data before displaying |
57+
| `--base-currency` | `USD` | Base currency for value conversion (case-insensitive, e.g., `USD`, `CAD`) |

0 commit comments

Comments
 (0)