Skip to content

Commit 8e99804

Browse files
committed
unsafe-sell
1 parent bdba525 commit 8e99804

File tree

11 files changed

+723
-224
lines changed

11 files changed

+723
-224
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ ibctl transaction list --from 20250101 --to 20251231 --base-currency USD
5757
| `ibctl holding category list` | Display holdings aggregated by category |
5858
| `ibctl holding geo list` | Display holdings aggregated by geographic classification |
5959
| `ibctl holding safe-sell list` | Identify positions safe to sell from a tax perspective (loss or LTCG only) |
60+
| `ibctl holding unsafe-sell list` | Show positions with short-term capital gains exposure |
6061
| `ibctl holding value` | Display portfolio value with estimated tax impact |
6162
| `ibctl transaction list` | List all transactions (buys, sells, dividends, interest, WHT, etc.) chronologically |
6263
| `ibctl transaction sale list` | List realized security sales with FIFO lot matching for tax reporting |

book/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
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 safe-sell list](commands/holding-safe-sell-list.md)
20+
- [holding unsafe-sell list](commands/holding-unsafe-sell-list.md)
2021
- [holding value](commands/holding-value.md)
2122
- [transaction list](commands/transaction-list.md)
2223
- [transaction sale list](commands/transaction-sale-list.md)

book/src/commands/holding-list.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Positions worth less than 0.01 in the base currency are filtered out. This remov
2424
| P&L {BASE} | Unrealized profit/loss in base currency (computed per lot, excluding tax-exempt lots, using date-specific FX rates) |
2525
| STCG {BASE} | Short-term unrealized capital gain in base currency (lots held < 365 days) |
2626
| LTCG {BASE} | Long-term unrealized capital gain in base currency (lots held >= 365 days) |
27-
| POSITION | Total quantity held across all accounts |
27+
| QTY | Total quantity held across all accounts |
2828
| CATEGORY | User-defined asset category from configuration (e.g., `EQUITY`, `FIXED_INCOME`) |
2929
| TYPE | User-defined asset type (e.g., `STOCK`, `ETF`) |
3030
| SECTOR | User-defined sector classification (e.g., `TECH`) |

book/src/commands/holding-safe-sell-list.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ The `safe_sell.max_stcg` setting in `ibctl.yaml` controls tolerance for short-te
3030
| ACCOUNT | Account alias (only shown with `--separate-accounts`) |
3131
| QTY | Total quantity across all sellable lots |
3232
| QTY REMAINING | Quantity not included because the FIFO walk was stopped by a lot exceeding the STCG threshold. Zero means all lots for this symbol are sellable. |
33+
| LTCG DATE | Date when all remaining lots will have been held >= 365 days and qualify as LTCG. Empty if there are no remaining STCG lots. |
3334
| CURRENCY | Native currency of cost basis and market price |
3435
| AVG PRICE | Weighted average cost basis per share in native currency |
3536
| MKT PRICE | Current market price per share in native currency |
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# holding unsafe-sell list
2+
3+
```
4+
ibctl holding unsafe-sell list [--dir DIR] [--format FORMAT] [--download] [--realtime] [--separate-accounts] [--base-currency CURRENCY]
5+
```
6+
7+
Shows positions with unrealized short-term capital gains — the inverse of `holding safe-sell list`. These are positions where selling would trigger STCG taxed at ordinary income rates. Use this to understand your STCG exposure and when each position's short-term lots will cross the 365-day threshold and become long-term.
8+
9+
## Account combining (IRS vs CRA)
10+
11+
By default, lots from all accounts for the same symbol are merged into a single FIFO queue sorted by date (IRS-correct for disregarded entities). Use `--separate-accounts` for CRA-style per-account FIFO.
12+
13+
## Columns
14+
15+
| Column | Description |
16+
|--------|-------------|
17+
| SYMBOL | Ticker symbol |
18+
| ACCOUNT | Account alias (only shown with `--separate-accounts`) |
19+
| QTY | Total quantity of lots with positive STCG |
20+
| LTCG DATE | Date when all STCG lots will have been held >= 365 days and qualify as LTCG |
21+
| CURRENCY | Native currency |
22+
| MKT PRICE | Current market price per share (native currency) |
23+
| MKT VAL {BASE} | Market value of the STCG lots (base currency) |
24+
| STCG {BASE} | Total short-term capital gain (base currency) |
25+
| CATEGORY | User-defined asset category from configuration |
26+
| TYPE | User-defined asset type |
27+
| SECTOR | User-defined sector classification |
28+
| GEO | User-defined geographic classification |
29+
30+
`{BASE}` is replaced with the `--base-currency` value (e.g., `MKT VAL USD`, `STCG CAD`).
31+
32+
## Sort order
33+
34+
Results are sorted by LTCG date ascending (soonest to become long-term first).
35+
36+
## Totals row
37+
38+
The table output includes a totals row summing MKT VAL {BASE} and STCG {BASE}.
39+
40+
## Examples
41+
42+
```bash
43+
# STCG exposure with real-time prices
44+
ibctl holding unsafe-sell list --realtime
45+
46+
# Per-account FIFO (CRA-style), JSON output
47+
ibctl holding unsafe-sell list --realtime --separate-accounts --format json
48+
```
49+
50+
## Flags
51+
52+
| Flag | Default | Description |
53+
|------|---------|-------------|
54+
| `--dir` | `.` | The ibctl directory containing `ibctl.yaml` |
55+
| `--format` | `table` | Output format: `table`, `csv`, or `json` |
56+
| `--download` | `false` | Download fresh data before displaying |
57+
| `--realtime` | `false` | Fetch real-time stock quotes and FX rates from Yahoo Finance |
58+
| `--separate-accounts` | `false` | Apply FIFO independently per account (CRA-style) |
59+
| `--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
@@ -35,6 +35,7 @@ All holding commands accept `--base-currency` (default `USD`) to convert values
3535
| `ibctl holding category list` | Aggregate holdings by category with market value and P&L |
3636
| `ibctl holding geo list` | Aggregate holdings by geographic classification with market value and P&L |
3737
| `ibctl holding safe-sell list` | Identify positions safe to sell from a tax perspective (loss or LTCG only) |
38+
| `ibctl holding unsafe-sell list` | Show positions with short-term capital gains exposure (inverse of safe-sell) |
3839
| `ibctl holding value` | Display portfolio value with estimated unrealized and YTD realized tax impact |
3940

4041
## Transaction commands

cmd/ibctl/internal/command/holding/holding.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/bufdev/ibctl/cmd/ibctl/internal/command/holding/lot"
1818
"github.com/bufdev/ibctl/cmd/ibctl/internal/command/holding/projectedincome"
1919
"github.com/bufdev/ibctl/cmd/ibctl/internal/command/holding/safesell"
20+
"github.com/bufdev/ibctl/cmd/ibctl/internal/command/holding/unsafesell"
2021
)
2122

2223
// NewCommand returns a new holding command group.
@@ -33,6 +34,7 @@ func NewCommand(name string, builder appext.SubCommandBuilder) *appcmd.Command {
3334
holdingoverview.NewCommand("overview", builder),
3435
projectedincome.NewCommand("projected-income", builder),
3536
safesell.NewCommand("safe-sell", builder),
37+
unsafesell.NewCommand("unsafe-sell", builder),
3638
holdingvalue.NewCommand("value", builder),
3739
},
3840
}

cmd/ibctl/internal/command/holding/safesell/safeselllist/safeselllist.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ COLUMNS
9393
ACCOUNT Account alias. Only shown with --separate-accounts.
9494
QTY Total quantity across all sellable lots.
9595
QTY REMAINING Quantity not included because the FIFO walk was stopped.
96+
LTCG DATE Date when all remaining lots qualify as LTCG (empty if none).
9697
CURRENCY Native currency of cost basis and market price.
9798
AVG PRICE Weighted average cost basis per share (native currency).
9899
MKT PRICE Current market price per share (native currency).
@@ -195,7 +196,6 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
195196
return err
196197
}
197198
}
198-
// Get the safe-sell list.
199199
// Get the safe-sell list. The STCG threshold is read from config.SafeSellMaxSTCGMicros.
200200
results, err := ibctlsafesell.GetSafeSellList(
201201
mergedData.Trades,
@@ -222,17 +222,19 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
222222
totalsRow := make([]string, len(headers))
223223
totalsRow[0] = "TOTAL"
224224
if flags.SeparateAccounts {
225-
// With ACCOUNT column: SYMBOL, ACCOUNT, QTY, QTY REMAINING, CURRENCY, AVG PRICE, MKT PRICE, MKT VAL, P&L, STCG, LTCG, ...
225+
// With ACCOUNT: SYMBOL(0), ACCOUNT(1), QTY(2), QTY REMAINING(3), LTCG DATE(4),
226+
// CURRENCY(5), AVG PRICE(6), MKT PRICE(7), MKT VAL(8), P&L(9), STCG(10), LTCG(11), ...
227+
totalsRow[8] = totals.MktValBase
228+
totalsRow[9] = totals.PnLBase
229+
totalsRow[10] = totals.STCGBase
230+
totalsRow[11] = totals.LTCGBase
231+
} else {
232+
// Without ACCOUNT: SYMBOL(0), QTY(1), QTY REMAINING(2), LTCG DATE(3),
233+
// CURRENCY(4), AVG PRICE(5), MKT PRICE(6), MKT VAL(7), P&L(8), STCG(9), LTCG(10), ...
226234
totalsRow[7] = totals.MktValBase
227235
totalsRow[8] = totals.PnLBase
228236
totalsRow[9] = totals.STCGBase
229237
totalsRow[10] = totals.LTCGBase
230-
} else {
231-
// Without ACCOUNT column: SYMBOL, QTY, QTY REMAINING, CURRENCY, AVG PRICE, MKT PRICE, MKT VAL, P&L, STCG, LTCG, ...
232-
totalsRow[6] = totals.MktValBase
233-
totalsRow[7] = totals.PnLBase
234-
totalsRow[8] = totals.STCGBase
235-
totalsRow[9] = totals.LTCGBase
236238
}
237239
return cliio.WriteTableWithTotals(writer, headers, rows, totalsRow)
238240
case cliio.FormatCSV:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2026 Peter Edge
2+
//
3+
// All rights reserved.
4+
5+
// Package unsafesell implements the "holding unsafe-sell" command group.
6+
package unsafesell
7+
8+
import (
9+
"buf.build/go/app/appcmd"
10+
"buf.build/go/app/appext"
11+
"github.com/bufdev/ibctl/cmd/ibctl/internal/command/holding/unsafesell/unsafeselllist"
12+
)
13+
14+
// NewCommand returns a new unsafe-sell command group.
15+
func NewCommand(name string, builder appext.SubCommandBuilder) *appcmd.Command {
16+
return &appcmd.Command{
17+
Use: name,
18+
Short: "Identify positions with short-term capital gains exposure",
19+
SubCommands: []*appcmd.Command{
20+
unsafeselllist.NewCommand("list", builder),
21+
},
22+
}
23+
}

0 commit comments

Comments
 (0)