You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: README.md
+67-14Lines changed: 67 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -52,7 +52,8 @@ Follow these exact steps in the IBKR portal to create a Flex Query and generate
52
52
| Path | Purpose | Override |
53
53
|------|---------|----------|
54
54
|`ibctl.yaml`| Configuration file in current directory |`--config` flag |
55
-
|`<data_dir>/v1/<account>/`| Per-account downloaded data cache | Set `data_dir` and `accounts` in config |
55
+
|`<data_dir>/v1/accounts/<alias>/`| Per-account downloaded data cache | Set `data_dir` and `accounts` in config |
56
+
|`<data_dir>/v1/fx/`| FX rate data per currency pair | Derived from `data_dir`|
56
57
57
58
## Environment Variables
58
59
@@ -134,7 +135,7 @@ ibctl download
134
135
ibctl probe
135
136
```
136
137
137
-
Data is downloaded automatically when commands need it. Use `ibctl download` to force a refresh. Each download merges new data with the existing cache — trades are deduplicated by trade ID, so it is safe to run repeatedly. Data is stored per account under `<data_dir>/v1/<account_alias>/`.
138
+
Data is downloaded automatically when commands need it. Use `ibctl download` to force a refresh. Each download merges new data with the existing cache — trades are deduplicated by trade ID, so it is safe to run repeatedly. Data is stored per account under `<data_dir>/v1/accounts/<alias>/`.
138
139
139
140
## Commands
140
141
@@ -204,24 +205,76 @@ To keep data current, the Flex Query API provides the latest 365 days. To add ol
204
205
205
206
ibctl supports multiple IBKR accounts via the `accounts` section in the config. Each account is identified by an alias (e.g., "rrsp", "holdco") that maps to an IBKR account ID.
206
207
207
-
-**Downloaded data** is stored per account under `<data_dir>/v1/<alias>/`
208
+
-**Downloaded data** is stored per account under `<data_dir>/v1/accounts/<alias>/`
208
209
-**Holdings overview** shows a combined view aggregated across all accounts
209
210
-**Transfers** between accounts and from other brokers (ACATS) are tracked and converted to synthetic trades for accurate FIFO computation
210
211
-**Corporate actions** (stock splits, mergers, spinoffs) are captured from the Flex Query API
211
212
212
213
Account numbers are confidential — only aliases appear in output and directory names.
213
214
214
-
## Data Storage
215
+
## Implementation
215
216
216
-
Raw API data is cached as protobuf-JSON files under per-account directories (`<data_dir>/v1/<alias>/`). Each file stores newline-separated proto JSON (one message per line), serialized using `protojson` with proto field names. Tax lots and derived computations are performed at read time from the merged data (Activity Statement CSVs + cached API data).
217
+
### Data Directory Structure
217
218
218
-
| File | Protobuf Message | Description |
219
-
|------|-----------------|-------------|
220
-
|`<alias>/trades.json`|`ibctl.data.v1.Trade`| All trades for this account. Includes trade ID, account, dates, symbol, side (buy/sell), quantity, price, proceeds, commission, currency code, and FIFO realized P&L. |
221
-
|`<alias>/positions.json`|`ibctl.data.v1.Position`| Open positions for this account, including quantity, cost basis price, market price, market value, currency code, and unrealized P&L. |
222
-
|`<alias>/transfers.json`|`ibctl.data.v1.Transfer`| Position transfers (ACATS, ATON, FOP, internal) for this account. Transfer-in records are converted to synthetic buy trades for FIFO processing. |
223
-
|`<alias>/trade_transfers.json`|`ibctl.data.v1.TradeTransfer`| Transferred trade cost basis records. Preserves original trade date and cost basis for positions transferred from other brokers. |
224
-
|`<alias>/corporate_actions.json`|`ibctl.data.v1.CorporateAction`| Corporate action events (stock splits, mergers, spinoffs) for this account. |
225
-
|`exchange_rates.json`|`ibctl.data.v1.ExchangeRate`| Currency exchange rates (shared across accounts) with date, base/quote currency codes, rate, and provider (ibkr or [frankfurter.dev](https://frankfurter.dev)). |
219
+
All cached data lives under `<data_dir>/v1/`. Per-account data is stored in `accounts/<alias>/`, and FX rate data is stored in `fx/`.
226
220
227
-
Monetary values use `standard.money.v1.Money` with units and micros (6 decimal places). Dates use `standard.time.v1.Date` with year, month, and day fields.
221
+
```
222
+
<data_dir>/v1/
223
+
├── accounts/ # Per-account cached data from IBKR Flex Query API.
224
+
│ ├── rrsp/
225
+
│ │ ├── trades.json # All trades (buys/sells) for this account.
226
+
│ │ ├── positions.json # Latest IBKR-reported open positions snapshot.
227
+
│ │ ├── transfers.json # Position transfers (ACATS, FOP, internal).
All files use newline-separated protobuf JSON (one message per line), serialized with `protojson` using proto field names. Monetary values use `standard.money.v1.Money` (units + micros for 6 decimal places). Dates use `standard.time.v1.Date` (year, month, day).
|`trades.json`|`ibctl.data.v1.Trade`| Deduplicated by trade ID, new overwrites old | All trades (buys/sells) for this account. Incrementally merged across downloads so the cache grows over time, overcoming IBKR's 365-day API limit. |
245
+
|`positions.json`|`ibctl.data.v1.Position`| Overwritten entirely on each download | Latest snapshot of IBKR-reported open positions. Provides current market prices (LAST PRICE column) and serves as verification data — computed holdings are compared against these to detect discrepancies. **Not the source of truth for quantities or cost basis** — those are computed via FIFO from trades. |
246
+
|`transfers.json`|`ibctl.data.v1.Transfer`| Overwritten on each download | Position transfers between accounts or brokers (ACATS, ATON, FOP, internal). Transfer-in records with a non-zero transfer price are converted to synthetic buy trades for FIFO processing. Transfer-out records become synthetic sell trades. Transfers without a price are informational only. |
247
+
|`trade_transfers.json`|`ibctl.data.v1.TradeTransfer`| Overwritten on each download | Cost basis and holding period metadata for positions transferred from other brokers. Preserves the **original trade date** (for long-term vs short-term capital gains) and **original cost basis** from the source broker. Converted to synthetic buy trades using the original date, not the transfer date. |
248
+
|`corporate_actions.json`|`ibctl.data.v1.CorporateAction`| Overwritten on each download | Corporate action events (forward/reverse splits, mergers, spinoffs) for audit purposes. |
249
+
|`exchange_rates.json`|`ibctl.data.v1.ExchangeRate`| Deduplicated by date + currency pair | FX rates from IBKR cash transactions and [frankfurter.dev](https://frankfurter.dev). Each rate has a date, base/quote currency codes, rate value, and provider field. |
250
+
251
+
### Seed Data
252
+
253
+
The optional `seed_dir` in the config points to a directory of permanent, manually curated data from previous brokers (e.g., UBS, RBC). This data is never modified by ibctl.
254
+
255
+
```
256
+
<seed_dir>/
257
+
└── <alias>/
258
+
└── transactions.json # Normalized transaction history from previous brokers.
259
+
```
260
+
261
+
`transactions.json` uses the `ibctl.data.v1.ImportedTransaction` proto, which represents all transaction types from the previous broker: buys, sells, splits, stock dividends, expiries, redemptions, dividends, interest, fees, withholding tax, transfers, deposits, and withdrawals. At read time, only security-affecting transactions (buys, sells, splits, expiries, redemptions, stock dividends) are converted to Trade protos for FIFO processing. The rest are stored for income tracking and audit.
262
+
263
+
### Data Pipeline
264
+
265
+
The `holdings overview` command runs the following pipeline:
266
+
267
+
1.**Download** (`ibctl download` or automatic): Fetches all accounts' data from the IBKR Flex Query API in a single API call. Trades are incrementally merged with the cache (deduplicated by trade ID). Positions are overwritten with the latest snapshot. FX rates are extracted from cash transactions and supplemented by [frankfurter.dev](https://frankfurter.dev) for any missing dates.
268
+
269
+
2.**Merge** (`ibctlmerge`): Combines three data sources per account, with CSV data taking precedence for overlapping date ranges:
270
+
-**Activity Statement CSVs** (`activity_statements_dir/<alias>/*.csv`) — primary source of truth for trades. These cover dates that the CSVs span; Flex Query trades within this range are excluded because the two sources represent the same trades at different granularities (CSVs consolidate order executions, Flex Query splits them).
271
+
-**Seed data** (`seed_dir/<alias>/transactions.json`) — imported transactions from previous brokers, converted to Trade protos for FIFO.
272
+
-**Flex Query cache** (`accounts/<alias>/trades.json`) — recent trades from the API, used only for dates not covered by CSVs.
273
+
274
+
3.**FIFO Tax Lot Computation** (`ibctltaxlot`): Processes all merged trades using First-In-First-Out ordering, grouped by (account, symbol). Transfers and trade transfers are converted to synthetic trades before processing. Buys create new lots, sells consume the oldest lots first. Short positions are supported (sell-to-open creates negative lots, buy-to-close closes them). Within the same date, buys are processed before sells to handle same-day buy+sell scenarios.
275
+
276
+
4.**Position Aggregation**: Tax lots are aggregated into per-account positions with weighted average cost basis, then combined across accounts for display.
277
+
278
+
5.**Verification**: Computed positions are compared against IBKR-reported positions (`positions.json`). Quantity mismatches and cost basis discrepancies exceeding 0.1% are logged as warnings. Positions that exist only in computed data or only in IBKR's report are also flagged.
279
+
280
+
6.**Display**: Holdings are rendered with current market prices from IBKR positions and optional symbol classifications from the config.
0 commit comments