Skip to content

Commit 0112cb1

Browse files
committed
feat: add --securities-on-date flag for historical portfolio reconstruction
Reconstructs securities positions as of any historical date using trade history FIFO and Yahoo Finance historical close prices. Positions are rebuilt from the full trade history filtered to the target date, with cached FX rates for currency conversion. New package ibctlhistorical provides FilterTradesByDate, BuildSnapshot, and ApplyHistoricalPrices. GetHistoricalClose added to the Yahoo Finance client. FetchHistoricalPrices added to ibctlrealtime for symbol mapping. Commands with --securities-on-date: holding list, category list, geo list, overview, value, lot list, possible-sale list. The value command scopes YTD realized gains to the target date's year. Flag validation and data loading branching abstracted into ValidateSecuritiesOnDate and LoadHoldingsDataForFlags in ibctlcmd. BuildPositionMap now accepts a symbolTypes map from config for accurate bond identification regardless of trade-derived AssetCategory values. Limitations documented in holding-list.md: - Cash excluded (trade history lacks deposit/withdrawal data) - Bonds and other Yahoo-unresolvable securities excluded with warnings - International tickers require realtime_symbols config mapping - Config addition prices (HOUSE) not used as fallback - FX rates depend on cache from prior downloads
1 parent a3c6769 commit 0112cb1

27 files changed

Lines changed: 1213 additions & 28 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/.tmp/
22
/book/book/
3+
/plans/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ ibctl transaction list --from 20250101 --to 20251231 --base-currency USD
6262
| `ibctl transaction list` | List all transactions (buys, sells, dividends, interest, WHT, etc.) chronologically |
6363
| `ibctl realized-sale list` | List realized security sales with FIFO lot matching for tax reporting |
6464

65-
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. Holding commands accept `--realtime` to fetch current stock prices and FX rates from Yahoo Finance on-demand. For international symbols where IBKR and Yahoo symbols differ, add mappings to `realtime_symbols` in `ibctl.yaml`. Use `--help` on any command for detailed documentation.
65+
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. Holding commands accept `--realtime` to fetch current stock prices and FX rates from Yahoo Finance on-demand. For international symbols where IBKR and Yahoo symbols differ, add mappings to `realtime_symbols` in `ibctl.yaml`. Holding commands accept `--securities-on-date YYYYMMDD` to reconstruct securities positions as of a historical date by replaying trades and fetching historical close prices from Yahoo Finance. The `--securities-on-date` flag is mutually exclusive with `--realtime` and `--download`. Use `--help` on any command for detailed documentation.
6666

6767
## Tax Reporting
6868

book/src/commands/category-list.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# category list
22

33
```
4-
ibctl category list [--dir DIR] [--format FORMAT] [--download] [--realtime] [--geo GEO] [--base-currency CURRENCY]
4+
ibctl category list [--dir DIR] [--format FORMAT] [--download] [--realtime] [--securities-on-date DATE] [--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.
@@ -39,5 +39,6 @@ Use `--geo` to filter holdings to a single geographic classification before aggr
3939
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
4040
| `--download` | `false` | Download fresh data before displaying |
4141
| `--realtime` | `false` | Fetch real-time stock quotes and FX rates from Yahoo Finance |
42+
| `--securities-on-date` | (today) | Show securities positions as of this date (YYYYMMDD format, excludes cash and unpriced securities) |
4243
| `--geo` | (all) | Filter by geographic classification before aggregating (e.g., `US`, `INTL`) |
4344
| `--base-currency` | `USD` | Base currency for value conversion (case-insensitive, e.g., `USD`, `CAD`) |

book/src/commands/geo-list.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# geo list
22

33
```
4-
ibctl geo list [--dir DIR] [--format FORMAT] [--download] [--realtime] [--category CATEGORY] [--base-currency CURRENCY]
4+
ibctl geo list [--dir DIR] [--format FORMAT] [--download] [--realtime] [--securities-on-date DATE] [--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.
@@ -39,5 +39,6 @@ Use `--category` to filter holdings to a single category before aggregating by g
3939
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
4040
| `--download` | `false` | Download fresh data before displaying |
4141
| `--realtime` | `false` | Fetch real-time stock quotes and FX rates from Yahoo Finance |
42+
| `--securities-on-date` | (today) | Show securities positions as of this date (YYYYMMDD format, excludes cash and unpriced securities) |
4243
| `--category` | (all) | Filter by category before aggregating (e.g., `EQUITY`, `FIXED_INCOME`) |
4344
| `--base-currency` | `USD` | Base currency for value conversion (case-insensitive, e.g., `USD`, `CAD`) |

book/src/commands/holding-list.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# holding list
22

33
```
4-
ibctl holding list [--dir DIR] [--format FORMAT] [--download] [--realtime] [--base-currency CURRENCY]
4+
ibctl holding list [--dir DIR] [--format FORMAT] [--download] [--realtime] [--securities-on-date DATE] [--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.
@@ -68,6 +68,21 @@ Use `--realtime` to fetch current market prices and FX rates from Yahoo Finance
6868

6969
For international symbols where IBKR and Yahoo Finance symbols differ (e.g., Canadian equities), add symbol mappings to the `realtime_symbols` section of your config. Real-time data is not cached — each run fetches fresh prices from Yahoo Finance.
7070

71+
## Historical portfolio reconstruction
72+
73+
Use `--securities-on-date YYYYMMDD` to reconstruct securities positions as of a historical date. Positions are rebuilt by replaying all trades up to that date using FIFO lot computation. Historical close prices are fetched from Yahoo Finance; FX rates use cached historical data.
74+
75+
The flag is mutually exclusive with `--realtime` and `--download`.
76+
77+
**Limitations:**
78+
79+
- **Cash excluded.** Trade history does not track deposits, withdrawals, or FX conversions. Cash positions (USD, CAD) and config cash adjustments are omitted from the output.
80+
- **Bonds and other Yahoo-unresolvable securities excluded.** Yahoo Finance does not quote individual corporate bonds. Any security that Yahoo cannot price is excluded with a warning. No config fallback prices or cost basis estimates are used — every price in the output is a real historical market price.
81+
- **International tickers require `realtime_symbols` mapping.** Tickers where IBKR and Yahoo symbols differ (e.g., HK-listed stocks) must be mapped in `ibctl.yaml` under `realtime_symbols`, the same mapping used by `--realtime`.
82+
- **Addition prices are not date-aware.** Config additions (like HOUSE) that Yahoo cannot price are excluded. The config `last_price` is a static current estimate, not a historical value.
83+
- **FX rates depend on cache.** Historical FX rates come from the frankfurter.dev/Bank of Canada cache. Dates before the first `ibctl download` may have missing rates.
84+
- **Position verification suppressed.** The normal FIFO-vs-IBKR verification is not meaningful for reconstructed positions and may produce spurious warnings.
85+
7186
## Flags
7287

7388
| Flag | Default | Description |
@@ -76,4 +91,5 @@ For international symbols where IBKR and Yahoo Finance symbols differ (e.g., Can
7691
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
7792
| `--download` | `false` | Download fresh data before displaying |
7893
| `--realtime` | `false` | Fetch real-time stock quotes and FX rates from Yahoo Finance |
94+
| `--securities-on-date` | (today) | Show securities positions as of this date (YYYYMMDD format, excludes cash and unpriced securities) |
7995
| `--base-currency` | `USD` | Base currency for value conversion (case-insensitive, e.g., `USD`, `CAD`) |

book/src/commands/lot-list.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# lot list
22

33
```
4-
ibctl lot list [--dir DIR] [--format FORMAT] [--download] [--realtime] [--symbol SYMBOL] [--base-currency CURRENCY]
4+
ibctl lot list [--dir DIR] [--format FORMAT] [--download] [--realtime] [--securities-on-date DATE] [--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.
@@ -52,5 +52,6 @@ The table output includes a totals row summing P&L {BASE}, STCG {BASE}, LTCG {BA
5252
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
5353
| `--download` | `false` | Download fresh data before displaying |
5454
| `--realtime` | `false` | Fetch real-time stock quotes and FX rates from Yahoo Finance |
55+
| `--securities-on-date` | (today) | Show securities positions as of this date (YYYYMMDD format, excludes cash and unpriced securities) |
5556
| `--symbol` | (all) | Filter to a specific symbol. Omit to show all symbols. |
5657
| `--base-currency` | `USD` | Base currency for value conversion (case-insensitive, e.g., `USD`, `CAD`) |

book/src/commands/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,5 @@ Several flags appear across multiple commands:
5959
| `--dir` | `.` | The ibctl directory containing `ibctl.yaml` |
6060
| `--download` | `false` | Download fresh data before displaying results |
6161
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
62+
| `--securities-on-date` | (today) | Show securities positions as of this date (YYYYMMDD format, excludes cash and unpriced securities). Available on holding commands (`holding list`, `lot list`, `category list`, `geo list`, `possible-sale list`, `value`). Mutually exclusive with `--realtime` and `--download`. |
6263
| `--base-currency` | `USD` | Base currency for value/P&L conversion (case-insensitive, e.g., `USD`, `CAD`). Available on all holding and transaction commands. |

book/src/commands/possible-sale-list.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# possible-sale list
22

33
```
4-
ibctl possible-sale list [--dir DIR] [--format FORMAT] [--download] [--realtime]
4+
ibctl possible-sale list [--dir DIR] [--format FORMAT] [--download] [--realtime] [--securities-on-date DATE]
55
[--safe | --safe-excluded | --unsafe] [--separate-accounts] [--base-currency CURRENCY]
66
```
77

@@ -125,6 +125,7 @@ ibctl possible-sale list --safe --realtime --base-currency CAD --format csv
125125
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
126126
| `--download` | `false` | Download fresh data before displaying |
127127
| `--realtime` | `false` | Fetch real-time stock quotes and FX rates from Yahoo Finance |
128+
| `--securities-on-date` | (today) | Show securities positions as of this date (YYYYMMDD format, excludes cash and unpriced securities) |
128129
| `--safe` | `false` | Show only positions safe to sell (loss or LTCG, respecting FIFO and exclusions) |
129130
| `--safe-excluded` | `false` | Show only positions excluded from the safe view |
130131
| `--unsafe` | `false` | Show only positions with STCG exposure (inverse of safe) |

book/src/commands/value.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# value
22

33
```
4-
ibctl value [--dir DIR] [--download] [--realtime] [--base-currency CURRENCY]
4+
ibctl value [--dir DIR] [--download] [--realtime] [--securities-on-date DATE] [--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.
@@ -100,4 +100,5 @@ taxes:
100100
| `--dir` | `.` | The ibctl directory containing `ibctl.yaml` |
101101
| `--download` | `false` | Download fresh data before displaying |
102102
| `--realtime` | `false` | Fetch real-time stock quotes and FX rates from Yahoo Finance |
103+
| `--securities-on-date` | (today) | Show securities positions as of this date (YYYYMMDD format, excludes cash and unpriced securities) |
103104
| `--base-currency` | `USD` | Base currency for value conversion (case-insensitive, e.g., `USD`, `CAD`) |

cmd/ibctl/internal/command/category/categorylist/categorylist.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ const (
2929
baseCurrencyFlagName = "base-currency"
3030
// realtimeFlagName is the flag name for fetching real-time quotes and FX rates.
3131
realtimeFlagName = "realtime"
32+
// securitiesOnDateFlagName is the flag name for viewing securities positions as of a historical date.
33+
securitiesOnDateFlagName = "securities-on-date"
3234
)
3335

3436
// NewCommand returns a new category list command.
@@ -70,6 +72,8 @@ type flags struct {
7072
BaseCurrency string
7173
// Realtime fetches real-time quotes and FX rates from Yahoo Finance.
7274
Realtime bool
75+
// SecuritiesOnDate is the historical date in YYYYMMDD format (empty means current).
76+
SecuritiesOnDate string
7377
}
7478

7579
func newFlags() *flags {
@@ -83,15 +87,22 @@ func (f *flags) Bind(flagSet *pflag.FlagSet) {
8387
flagSet.StringVar(&f.Geo, geoFlagName, "", "Filter by geo (e.g., US, INTL)")
8488
flagSet.StringVar(&f.BaseCurrency, baseCurrencyFlagName, "USD", "Base currency for value conversion (e.g., USD, CAD)")
8589
flagSet.BoolVar(&f.Realtime, realtimeFlagName, false, "Fetch real-time quotes and FX rates from Yahoo Finance")
90+
// Register --securities-on-date for historical securities position reconstruction.
91+
flagSet.StringVar(&f.SecuritiesOnDate, securitiesOnDateFlagName, "", "Show securities positions as of this date (YYYYMMDD format, excludes cash)")
8692
}
8793

8894
func run(ctx context.Context, container appext.Container, flags *flags) error {
8995
format, err := cliio.ParseFormat(flags.Format)
9096
if err != nil {
9197
return appcmd.NewInvalidArgumentError(err.Error())
9298
}
93-
// Load data through the common pipeline (config, merge, FX, optional download/realtime).
94-
data, err := ibctlcmd.LoadHoldingsData(ctx, container, flags.Download, flags.Realtime, flags.BaseCurrency)
99+
// Validate --securities-on-date is not combined with --realtime or --download.
100+
if err := ibctlcmd.ValidateSecuritiesOnDate(flags.SecuritiesOnDate, flags.Realtime, flags.Download); err != nil {
101+
return appcmd.NewInvalidArgumentError(err.Error())
102+
}
103+
// Load data through the common pipeline, branching on --securities-on-date.
104+
var data *ibctlcmd.HoldingsData
105+
data, err = ibctlcmd.LoadHoldingsDataForFlags(ctx, container, flags.Download, flags.Realtime, flags.BaseCurrency, flags.SecuritiesOnDate)
95106
if err != nil {
96107
return err
97108
}

0 commit comments

Comments
 (0)