Skip to content

Commit 76b4f5c

Browse files
committed
commit
1 parent 5e97ee3 commit 76b4f5c

File tree

4 files changed

+94
-67
lines changed

4 files changed

+94
-67
lines changed

book/src/commands/transaction-list.md

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Sells use the same FIFO lot matching as `transaction sale list`. A single sell o
2828

2929
## Columns
3030

31-
The output has 19 columns. Columns that don't apply to a given transaction type are left empty.
31+
The output has 21 columns. Columns that don't apply to a given transaction type are left empty.
3232

3333
| # | Column | Description |
3434
|---|--------|-------------|
@@ -41,26 +41,28 @@ The output has 19 columns. Columns that don't apply to a given transaction type
4141
| 7 | PUR PRICE | Cost basis per share in native currency (SELL rows only) |
4242
| 8 | DATE | Transaction date (YYYY-MM-DD) |
4343
| 9 | PRICE | Price per share in native currency (BUY and SELL rows only) |
44-
| 10 | INCOME | Income amount in native currency (DIVIDEND, INTEREST, and WHT rows). The TYPE column distinguishes them. WHT amounts are negative. |
45-
| 11 | P&L | Realized P&L in native currency (SELL rows only) |
46-
| 12 | PUR RATE {BASE} | FX rate on the purchase date (SELL rows only) |
47-
| 13 | RATE {BASE} | FX rate on the transaction date |
48-
| 14 | PUR {BASE} | Purchase price in base currency (SELL rows only) |
49-
| 15 | PRICE {BASE} | Price in base currency (BUY and SELL rows only) |
50-
| 16 | INCOME {BASE} | Income amount in base currency (DIVIDEND, INTEREST, and WHT rows) |
51-
| 17 | P&L {BASE} | Realized P&L in base currency (SELL rows only) |
52-
| 18 | STCG {BASE} | Short-term capital gain in base currency (SELL rows held < 365 days) |
53-
| 19 | LTCG {BASE} | Long-term capital gain in base currency (SELL rows held >= 365 days) |
54-
55-
`{BASE}` is replaced with the `--base-currency` value (e.g., `PUR RATE USD`, `INCOME CAD`).
44+
| 10 | DIVIDEND | Dividend amount in native currency (DIVIDEND and WHT rows). WHT amounts are negative, reducing the dividend total. |
45+
| 11 | INTEREST | Interest amount in native currency (INTEREST rows only) |
46+
| 12 | P&L | Realized P&L in native currency (SELL rows only) |
47+
| 13 | PUR RATE {BASE} | FX rate on the purchase date (SELL rows only) |
48+
| 14 | RATE {BASE} | FX rate on the transaction date |
49+
| 15 | PUR {BASE} | Purchase price in base currency (SELL rows only) |
50+
| 16 | PRICE {BASE} | Price in base currency (BUY and SELL rows only) |
51+
| 17 | DIV {BASE} | Dividend amount in base currency (DIVIDEND and WHT rows) |
52+
| 18 | INT {BASE} | Interest amount in base currency (INTEREST rows only) |
53+
| 19 | P&L {BASE} | Realized P&L in base currency (SELL rows only) |
54+
| 20 | STCG {BASE} | Short-term capital gain in base currency (SELL rows held < 365 days) |
55+
| 21 | LTCG {BASE} | Long-term capital gain in base currency (SELL rows held >= 365 days) |
56+
57+
`{BASE}` is replaced with the `--base-currency` value (e.g., `PUR RATE USD`, `DIV CAD`).
5658

5759
## Sort order
5860

5961
Rows are sorted by transaction date ascending, then by type, then by symbol. This produces a chronological view of all investment activity.
6062

6163
## Totals row
6264

63-
In table output, a totals row sums INCOME {BASE}, P&L {BASE}, STCG {BASE}, and LTCG {BASE} across all rows. This gives a quick summary of total income received, total realized gains/losses, and the STCG/LTCG split for the period.
65+
In table output, a totals row sums DIV {BASE}, INT {BASE}, P&L {BASE}, STCG {BASE}, and LTCG {BASE} across all rows. This gives a quick summary of total dividends, total interest, total realized gains/losses, and the STCG/LTCG split for the period.
6466

6567
## Income handling
6668

@@ -69,7 +71,7 @@ Income data comes from two sources:
6971
- **Flex Query CashTransactions** -- Dividends, interest, and withholding tax from the IBKR Flex Query are converted to Income protos and persisted in the data directory during download.
7072
- **Activity Statement CSVs** -- Dividends, interest, and withholding tax from Activity Statement CSV files are read at command time and merged with Flex Query income data.
7173

72-
Each income record has a type (DIVIDEND, INTEREST, or WHT), an amount in native currency, a symbol, an account, and a date. WHT amounts are negative (they represent tax withheld). The INCOME column in the output shows the amount, and the TYPE column distinguishes between the three income types.
74+
Each income record has a type (DIVIDEND, INTEREST, or WHT), an amount in native currency, a symbol, an account, and a date. WHT amounts are negative (they represent tax withheld). Dividends and WHT populate the DIVIDEND column; interest populates the INTEREST column. The TYPE column distinguishes the specific income type within each column.
7375

7476
## FIFO matching for sells
7577

book/src/tax-reporting.md

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ The CSV does not include a totals row. To compute totals, sum P&L {BASE}, STCG {
7878

7979
## Transaction list: column reference
8080

81-
This file has 19 columns. Each row is one transaction event in chronological order.
81+
This file has 21 columns. Each row is one transaction event in chronological order.
8282

8383
| # | Column Header | Description |
8484
|---|---------------|-------------|
@@ -91,16 +91,18 @@ This file has 19 columns. Each row is one transaction event in chronological ord
9191
| 7 | PUR PRICE | Cost basis per share in native currency. Only populated for SELL rows. |
9292
| 8 | DATE | Transaction date (YYYY-MM-DD) |
9393
| 9 | PRICE | Price per share in native currency. Only populated for BUY and SELL rows. |
94-
| 10 | INCOME | Income amount in native currency. Only populated for DIVIDEND, INTEREST, and WHT rows. WHT values are negative. |
95-
| 11 | P&L | Realized P&L in native currency. Only populated for SELL rows. |
96-
| 12 | PUR RATE {BASE} | FX rate on the purchase date. Only populated for SELL rows. |
97-
| 13 | RATE {BASE} | FX rate on the transaction date. Populated for all rows. |
98-
| 14 | PUR {BASE} | Purchase price per share in base currency. Only populated for SELL rows. |
99-
| 15 | PRICE {BASE} | Price per share in base currency. Only populated for BUY and SELL rows. |
100-
| 16 | INCOME {BASE} | Income amount in base currency. Only populated for DIVIDEND, INTEREST, and WHT rows. |
101-
| 17 | P&L {BASE} | Realized P&L in base currency. Only populated for SELL rows. |
102-
| 18 | STCG {BASE} | Short-term capital gain in base currency. SELL rows held < 365 days. |
103-
| 19 | LTCG {BASE} | Long-term capital gain in base currency. SELL rows held >= 365 days. |
94+
| 10 | DIVIDEND | Dividend amount in native currency. Only populated for DIVIDEND and WHT rows. WHT values are negative, reducing the dividend total. |
95+
| 11 | INTEREST | Interest amount in native currency. Only populated for INTEREST rows. |
96+
| 12 | P&L | Realized P&L in native currency. Only populated for SELL rows. |
97+
| 13 | PUR RATE {BASE} | FX rate on the purchase date. Only populated for SELL rows. |
98+
| 14 | RATE {BASE} | FX rate on the transaction date. Populated for all rows. |
99+
| 15 | PUR {BASE} | Purchase price per share in base currency. Only populated for SELL rows. |
100+
| 16 | PRICE {BASE} | Price per share in base currency. Only populated for BUY and SELL rows. |
101+
| 17 | DIV {BASE} | Dividend amount in base currency. Only populated for DIVIDEND and WHT rows. |
102+
| 18 | INT {BASE} | Interest amount in base currency. Only populated for INTEREST rows. |
103+
| 19 | P&L {BASE} | Realized P&L in base currency. Only populated for SELL rows. |
104+
| 20 | STCG {BASE} | Short-term capital gain in base currency. SELL rows held < 365 days. |
105+
| 21 | LTCG {BASE} | Long-term capital gain in base currency. SELL rows held >= 365 days. |
104106

105107
### Transaction types
106108

@@ -123,22 +125,23 @@ SELL rows use FIFO lot matching (identical to the transaction sale list), so a s
123125

124126
Not all columns are populated for every row type. In particular:
125127

126-
- **BUY**: PRICE and PRICE {BASE} are populated. PUR DATE, PUR PRICE, INCOME, and P&L columns are blank.
128+
- **BUY**: PRICE and PRICE {BASE} are populated. PUR DATE, PUR PRICE, DIVIDEND, INTEREST, and P&L columns are blank.
127129
- **SELL**: All columns may be populated, including purchase info from the matched buy lot.
128-
- **DIVIDEND / INTEREST / WHT**: INCOME and INCOME {BASE} are populated. QTY, PRICE, and P&L columns are blank.
129-
- **SPLIT / MERGER / SPINOFF / TRANSFER_IN / TRANSFER_OUT**: QTY is populated. PRICE, INCOME, and P&L columns are blank. These are informational rows.
130+
- **DIVIDEND / WHT**: DIVIDEND and DIV {BASE} are populated. WHT values are negative. QTY, PRICE, INTEREST, and P&L columns are blank.
131+
- **INTEREST**: INTEREST and INT {BASE} are populated. QTY, PRICE, DIVIDEND, and P&L columns are blank.
132+
- **SPLIT / MERGER / SPINOFF / TRANSFER_IN / TRANSFER_OUT**: QTY is populated. PRICE, DIVIDEND, INTEREST, and P&L columns are blank. These are informational rows.
130133

131134
### Totals
132135

133-
The CSV does not include a totals row. To compute totals, sum INCOME {BASE}, P&L {BASE}, STCG {BASE}, and LTCG {BASE} across all rows. This gives total income (dividends + interest - withholding tax), total realized gains/losses, and the STCG/LTCG split.
136+
The CSV does not include a totals row. To compute totals, sum DIV {BASE}, INT {BASE}, P&L {BASE}, STCG {BASE}, and LTCG {BASE} across all rows. This gives total dividends (net of WHT), total interest, total realized gains/losses, and the STCG/LTCG split.
134137

135138
## FX rate methodology
136139

137140
For cross-currency trades, native prices are converted to the base currency using date-specific FX rates:
138141

139142
- **Purchase price in base currency** = native purchase price multiplied by the FX rate on the purchase date.
140143
- **Sale price in base currency** = native sale price multiplied by the FX rate on the sale date.
141-
- **Income in base currency** = native income amount multiplied by the FX rate on the payment date.
144+
- **Dividend/interest in base currency** = native amount multiplied by the FX rate on the payment date.
142145
- **P&L in base currency** = `(sale price in base - purchase price in base) * quantity`.
143146

144147
This means the base-currency P&L captures both the underlying price movement of the security and the FX movement between the purchase and sale dates.

cmd/ibctl/internal/command/transaction/transactionlist/transactionlist.go

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ WHAT THIS COMMAND DOES
5858
5959
Income data comes from two sources: Flex Query CashTransactions (persisted
6060
as Income protos during download) and Activity Statement CSVs (read at
61-
command time). Dividends, interest, and withholding tax all appear in the
62-
INCOME column, distinguished by the TYPE column.
61+
command time). Dividends and WHT appear in the DIVIDEND column; interest
62+
appears in the INTEREST column.
6363
64-
COLUMNS (19)
64+
COLUMNS (21)
6565
6666
TYPE Transaction type (BUY, SELL, DIVIDEND, INTEREST, WHT,
6767
SPLIT, MERGER, SPINOFF, TRANSFER_IN, TRANSFER_OUT)
@@ -73,14 +73,16 @@ COLUMNS (19)
7373
PUR PRICE Cost basis per share in native currency (sells only)
7474
DATE Transaction date (YYYY-MM-DD)
7575
PRICE Price per share in native currency (buys and sells)
76-
INCOME Income amount in native currency (dividend, interest,
77-
or WHT -- TYPE distinguishes them; WHT is negative)
76+
DIVIDEND Dividend amount in native currency (DIVIDEND and WHT
77+
rows; WHT is negative, reducing the dividend total)
78+
INTEREST Interest amount in native currency (INTEREST rows only)
7879
P&L Realized P&L in native currency (sells only)
7980
PUR RATE {BASE} FX rate on purchase date (sells only)
8081
RATE {BASE} FX rate on transaction date
8182
PUR {BASE} Purchase price in base currency (sells only)
8283
PRICE {BASE} Price in base currency (buys and sells)
83-
INCOME {BASE} Income amount in base currency
84+
DIV {BASE} Dividend amount in base currency
85+
INT {BASE} Interest amount in base currency
8486
P&L {BASE} P&L in base currency (sells only)
8587
STCG {BASE} Short-term capital gain (held < 365 days)
8688
LTCG {BASE} Long-term capital gain (held >= 365 days)
@@ -103,7 +105,7 @@ TAX REPORTING (CSV OUTPUT)
103105
104106
TOTALS
105107
106-
The totals row sums: INCOME {BASE}, P&L {BASE}, STCG {BASE},
108+
The totals row sums: DIV {BASE}, INT {BASE}, P&L {BASE}, STCG {BASE},
107109
LTCG {BASE}.`,
108110
Args: appcmd.NoArgs,
109111
Run: builder.NewRunFunc(
@@ -226,15 +228,16 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
226228
for _, t := range result.Transactions {
227229
rows = append(rows, ibctltransactions.TransactionOverviewToTableRow(t, formatBase))
228230
}
229-
// Build totals row with sums for income/pnl/stcg/ltcg in base currency.
231+
// Build totals row with sums for dividend/interest/pnl/stcg/ltcg in base currency.
230232
totals := ibctltransactions.ComputeTotals(result.Transactions, formatBaseMicros)
231233
totalsRow := make([]string, len(headers))
232234
totalsRow[0] = "TOTAL"
233-
// INCOME BASE at column 15, P&L BASE at 16, STCG BASE at 17, LTCG BASE at 18.
234-
totalsRow[15] = totals.IncomeBase
235-
totalsRow[16] = totals.PnLBase
236-
totalsRow[17] = totals.STCGBase
237-
totalsRow[18] = totals.LTCGBase
235+
// DIV BASE at column 16, INT BASE at 17, P&L BASE at 18, STCG BASE at 19, LTCG BASE at 20.
236+
totalsRow[16] = totals.DividendBase
237+
totalsRow[17] = totals.InterestBase
238+
totalsRow[18] = totals.PnLBase
239+
totalsRow[19] = totals.STCGBase
240+
totalsRow[20] = totals.LTCGBase
238241
return cliio.WriteTableWithTotals(writer, headers, rows, totalsRow)
239242
case cliio.FormatCSV:
240243
headers := ibctltransactions.TransactionOverviewHeaders(baseCurrency)

internal/ibctl/ibctltransactions/ibctltransactions.go

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ type TransactionOverview struct {
6161
Date string `json:"date"`
6262
// Price is the price per share in native currency. Only for buys and sells.
6363
Price string `json:"price"`
64-
// Income is the income amount in native currency (dividend, interest, or WHT). TYPE distinguishes them.
65-
Income string `json:"income"`
64+
// Dividend is the dividend amount in native currency. Only for DIVIDEND and WHT rows.
65+
Dividend string `json:"dividend"`
66+
// Interest is the interest amount in native currency. Only for INTEREST rows.
67+
Interest string `json:"interest"`
6668
// PnL is the realized P&L in native currency. Only for sells.
6769
PnL string `json:"pnl"`
6870
// PurchaseRateBase is the FX rate on the buy date. Only for sells.
@@ -73,8 +75,10 @@ type TransactionOverview struct {
7375
PurchasePriceBase string `json:"purchase_price_base"`
7476
// PriceBase is the price in base currency. Only for buys and sells.
7577
PriceBase string `json:"price_base"`
76-
// IncomeBase is the income amount in base currency. TYPE distinguishes dividend/interest/WHT.
77-
IncomeBase string `json:"income_base"`
78+
// DividendBase is the dividend amount in base currency. Only for DIVIDEND and WHT rows.
79+
DividendBase string `json:"dividend_base"`
80+
// InterestBase is the interest amount in base currency. Only for INTEREST rows.
81+
InterestBase string `json:"interest_base"`
7882
// PnLBase is the realized P&L in base currency. Only for sells.
7983
PnLBase string `json:"pnl_base"`
8084
// STCGBase is the short-term capital gain in base currency (held < 365 days). Only for sells.
@@ -90,10 +94,10 @@ func TransactionOverviewHeaders(baseCurrency string) []string {
9094
"TYPE", "SYMBOL", "ACCOUNT", "QTY", "CCY",
9195
"PUR DATE", "PUR PRICE",
9296
"DATE", "PRICE",
93-
"INCOME", "P&L",
97+
"DIVIDEND", "INTEREST", "P&L",
9498
"PUR RATE " + baseCurrency, "RATE " + baseCurrency,
9599
"PUR " + baseCurrency, "PRICE " + baseCurrency,
96-
"INCOME " + baseCurrency,
100+
"DIV " + baseCurrency, "INT " + baseCurrency,
97101
"P&L " + baseCurrency, "STCG " + baseCurrency, "LTCG " + baseCurrency,
98102
}
99103
}
@@ -105,10 +109,10 @@ func TransactionOverviewToRow(t *TransactionOverview) []string {
105109
t.Type, t.Symbol, t.Account, t.Quantity, t.Currency,
106110
t.PurchaseDate, t.PurchasePrice,
107111
t.Date, t.Price,
108-
t.Income, t.PnL,
112+
t.Dividend, t.Interest, t.PnL,
109113
t.PurchaseRateBase, t.RateBase,
110114
t.PurchasePriceBase, t.PriceBase,
111-
t.IncomeBase,
115+
t.DividendBase, t.InterestBase,
112116
t.PnLBase, t.STCGBase, t.LTCGBase,
113117
}
114118
}
@@ -120,18 +124,20 @@ func TransactionOverviewToTableRow(t *TransactionOverview, formatBase func(strin
120124
t.Type, t.Symbol, t.Account, t.Quantity, t.Currency,
121125
t.PurchaseDate, t.PurchasePrice,
122126
t.Date, t.Price,
123-
t.Income, t.PnL,
127+
t.Dividend, t.Interest, t.PnL,
124128
t.PurchaseRateBase, t.RateBase,
125129
formatBase(t.PurchasePriceBase), formatBase(t.PriceBase),
126-
formatBase(t.IncomeBase),
130+
formatBase(t.DividendBase), formatBase(t.InterestBase),
127131
formatBase(t.PnLBase), formatBase(t.STCGBase), formatBase(t.LTCGBase),
128132
}
129133
}
130134

131135
// Totals holds the formatted total values for the transaction list summary row.
132136
type Totals struct {
133-
// IncomeBase is the total income (dividends + interest + WHT) in base currency.
134-
IncomeBase string
137+
// DividendBase is the total dividends (including WHT) in base currency.
138+
DividendBase string
139+
// InterestBase is the total interest income in base currency.
140+
InterestBase string
135141
// PnLBase is the total realized P&L in base currency.
136142
PnLBase string
137143
// STCGBase is the total short-term capital gain in base currency.
@@ -140,21 +146,23 @@ type Totals struct {
140146
LTCGBase string
141147
}
142148

143-
// ComputeTotals sums the income, P&L, STCG, and LTCG columns across all transactions.
149+
// ComputeTotals sums the dividend, interest, P&L, STCG, and LTCG columns across all transactions.
144150
// The formatBase function formats a micros value as a display string (e.g., FormatUSDMicros).
145151
func ComputeTotals(transactions []*TransactionOverview, formatBase func(int64) string) *Totals {
146-
var income, pnl, stcg, ltcg int64
152+
var dividend, interest, pnl, stcg, ltcg int64
147153
for _, t := range transactions {
148-
income += mathpb.ParseMicros(t.IncomeBase)
154+
dividend += mathpb.ParseMicros(t.DividendBase)
155+
interest += mathpb.ParseMicros(t.InterestBase)
149156
pnl += mathpb.ParseMicros(t.PnLBase)
150157
stcg += mathpb.ParseMicros(t.STCGBase)
151158
ltcg += mathpb.ParseMicros(t.LTCGBase)
152159
}
153160
return &Totals{
154-
IncomeBase: formatBase(income),
155-
PnLBase: formatBase(pnl),
156-
STCGBase: formatBase(stcg),
157-
LTCGBase: formatBase(ltcg),
161+
DividendBase: formatBase(dividend),
162+
InterestBase: formatBase(interest),
163+
PnLBase: formatBase(pnl),
164+
STCGBase: formatBase(stcg),
165+
LTCGBase: formatBase(ltcg),
158166
}
159167
}
160168

@@ -393,25 +401,36 @@ func buildIncomeTransactions(
393401
Date: incomeDateStr,
394402
}
395403
// Map income type to transaction type and populate the correct amount column.
404+
// Dividends and WHT populate the Dividend column; interest populates the Interest column.
396405
switch inc.GetType() {
397406
case datav1.IncomeType_INCOME_TYPE_DIVIDEND:
398407
overview.Type = "DIVIDEND"
408+
overview.Dividend = amountStr
399409
case datav1.IncomeType_INCOME_TYPE_INTEREST:
400410
overview.Type = "INTEREST"
411+
overview.Interest = amountStr
401412
case datav1.IncomeType_INCOME_TYPE_WITHHOLDING_TAX:
413+
// WHT is tax withheld on dividends, so it reduces the dividend total.
402414
overview.Type = "WHT"
415+
overview.Dividend = amountStr
403416
case datav1.IncomeType_INCOME_TYPE_UNSPECIFIED:
404417
continue
405418
}
406-
// Set the combined income amount in native currency.
407-
overview.Income = amountStr
408419
// Look up the FX rate on the income date for base currency conversion.
409420
rate, rateOK := fxStore.RateOnOrBefore(currencyCode, baseCurrency, incomeDateStr)
410421
overview.RateBase = formatRateMicros(rate, rateOK)
411422
if rateOK {
412-
// Convert the income amount to base currency.
423+
// Convert the income amount to base currency and populate the matching column.
413424
amountBase := multiplyMicros(amountMicros, rate)
414-
overview.IncomeBase = moneypb.MoneyValueToString(moneypb.MoneyFromMicros(baseCurrency, amountBase))
425+
amountBaseStr := moneypb.MoneyValueToString(moneypb.MoneyFromMicros(baseCurrency, amountBase))
426+
switch inc.GetType() {
427+
case datav1.IncomeType_INCOME_TYPE_DIVIDEND, datav1.IncomeType_INCOME_TYPE_WITHHOLDING_TAX:
428+
overview.DividendBase = amountBaseStr
429+
case datav1.IncomeType_INCOME_TYPE_INTEREST:
430+
overview.InterestBase = amountBaseStr
431+
case datav1.IncomeType_INCOME_TYPE_UNSPECIFIED:
432+
// Already filtered out above.
433+
}
415434
}
416435
transactions = append(transactions, overview)
417436
}

0 commit comments

Comments
 (0)