Skip to content

Commit 4c802cf

Browse files
committed
commit
1 parent 9b8b2f3 commit 4c802cf

2 files changed

Lines changed: 62 additions & 3 deletions

File tree

cmd/ibctl/internal/command/holdings/holdingsoverview/holdingsoverview.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,17 @@ func run(ctx context.Context, container appext.Container, flags *flags) error {
114114
switch format {
115115
case cliio.FormatTable:
116116
headers := ibctlholdings.HoldingsOverviewHeaders()
117-
rows := make([][]string, 0, len(result.Holdings))
117+
rows := make([][]string, 0, len(result.Holdings)+1)
118118
for _, h := range result.Holdings {
119119
rows = append(rows, ibctlholdings.HoldingOverviewToRow(h))
120120
}
121+
// Append a totals row summing MKT VAL USD and UNRLZD P&L USD.
122+
totalMktVal, totalPnL := ibctlholdings.ComputeTotals(result.Holdings)
123+
totalsRow := make([]string, len(headers))
124+
totalsRow[0] = "TOTAL"
125+
totalsRow[6] = totalMktVal
126+
totalsRow[7] = totalPnL
127+
rows = append(rows, totalsRow)
121128
return cliio.WriteTable(writer, headers, rows)
122129
case cliio.FormatCSV:
123130
headers := ibctlholdings.HoldingsOverviewHeaders()

internal/ibctl/ibctlholdings/ibctlholdings.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ type HoldingOverview struct {
4646
LastPriceUSD string `json:"last_price_usd,omitempty"`
4747
// AveragePriceUSD is the average cost basis price converted to USD.
4848
AveragePriceUSD string `json:"average_price_usd,omitempty"`
49+
// MarketValueUSD is position * last price USD.
50+
MarketValueUSD string `json:"market_value_usd,omitempty"`
51+
// UnrealizedPnLUSD is (last price USD - avg price USD) * position.
52+
UnrealizedPnLUSD string `json:"unrealized_pnl_usd,omitempty"`
4953
// Position is the total quantity held.
5054
Position *mathv1.Decimal `json:"position"`
5155
// Category is the user-defined asset category (e.g., "EQUITY").
@@ -58,7 +62,7 @@ type HoldingOverview struct {
5862

5963
// HoldingsOverviewHeaders returns the column headers for table/CSV output.
6064
func HoldingsOverviewHeaders() []string {
61-
return []string{"SYMBOL", "CURRENCY", "LAST PRICE", "AVG PRICE", "LAST USD", "AVG USD", "POSITION", "CATEGORY", "TYPE", "SECTOR"}
65+
return []string{"SYMBOL", "CURRENCY", "LAST PRICE", "AVG PRICE", "LAST USD", "AVG USD", "MKT VAL USD", "UNRLZD P&L USD", "POSITION", "CATEGORY", "TYPE", "SECTOR"}
6266
}
6367

6468
// HoldingOverviewToRow converts a HoldingOverview to a string slice for table/CSV output.
@@ -70,13 +74,35 @@ func HoldingOverviewToRow(h *HoldingOverview) []string {
7074
h.AveragePrice,
7175
h.LastPriceUSD,
7276
h.AveragePriceUSD,
77+
h.MarketValueUSD,
78+
h.UnrealizedPnLUSD,
7379
mathpb.ToString(h.Position),
7480
h.Category,
7581
h.Type,
7682
h.Sector,
7783
}
7884
}
7985

86+
// ComputeTotals sums the MarketValueUSD and UnrealizedPnLUSD across all holdings.
87+
// Returns formatted strings for the totals row.
88+
func ComputeTotals(holdings []*HoldingOverview) (string, string) {
89+
var totalMktValMicros, totalPnLMicros int64
90+
for _, h := range holdings {
91+
if h.MarketValueUSD != "" {
92+
if units, micros, err := mathpb.ParseToUnitsMicros(h.MarketValueUSD); err == nil {
93+
totalMktValMicros += units*1_000_000 + micros
94+
}
95+
}
96+
if h.UnrealizedPnLUSD != "" {
97+
if units, micros, err := mathpb.ParseToUnitsMicros(h.UnrealizedPnLUSD); err == nil {
98+
totalPnLMicros += units*1_000_000 + micros
99+
}
100+
}
101+
}
102+
return moneypb.MoneyValueToString(moneypb.MoneyFromMicros("USD", totalMktValMicros)),
103+
moneypb.MoneyValueToString(moneypb.MoneyFromMicros("USD", totalPnLMicros))
104+
}
105+
80106
// GetHoldingsOverview computes the holdings overview from trade data using FIFO,
81107
// then verifies against IBKR-reported positions.
82108
// The result is a combined view aggregated across all accounts.
@@ -176,16 +202,42 @@ func GetHoldingsOverview(
176202
AveragePrice: moneypb.MoneyValueToString(avgCostMoney),
177203
Position: mathpb.FromMicros(data.quantityMicros),
178204
}
179-
// Convert prices to USD using the most recent FX rate.
205+
// Convert prices to USD using the most recent FX rate, then compute
206+
// market value and unrealized P&L in USD.
180207
if fxStore != nil {
208+
var lastPriceUSDMicros, avgPriceUSDMicros int64
181209
if priceData.money != nil {
182210
if usdMoney, ok := fxStore.ConvertToUSD(priceData.money.GetMarketPrice()); ok {
211+
lastPriceUSDMicros = moneypb.MoneyToMicros(usdMoney)
183212
holding.LastPriceUSD = moneypb.MoneyValueToString(usdMoney)
184213
}
185214
}
186215
if usdMoney, ok := fxStore.ConvertToUSD(avgCostMoney); ok {
216+
avgPriceUSDMicros = moneypb.MoneyToMicros(usdMoney)
187217
holding.AveragePriceUSD = moneypb.MoneyValueToString(usdMoney)
188218
}
219+
// Market value USD = last price USD * position.
220+
// Bond prices are percentages of par, so divide by 100 for bonds.
221+
// Divide quantity first to avoid int64 overflow with large bond face values.
222+
isBond := priceData.money != nil && priceData.money.GetAssetCategory() == "BOND"
223+
if lastPriceUSDMicros != 0 {
224+
qtyRemainder := data.quantityMicros % 1_000_000
225+
mktValMicros := lastPriceUSDMicros*qtyUnits + lastPriceUSDMicros*qtyRemainder/1_000_000
226+
if isBond {
227+
mktValMicros /= 100
228+
}
229+
holding.MarketValueUSD = moneypb.MoneyValueToString(moneypb.MoneyFromMicros("USD", mktValMicros))
230+
}
231+
// Unrealized P&L USD = (last price USD - avg price USD) * position.
232+
if lastPriceUSDMicros != 0 && avgPriceUSDMicros != 0 {
233+
pnlPerShareMicros := lastPriceUSDMicros - avgPriceUSDMicros
234+
qtyRemainder := data.quantityMicros % 1_000_000
235+
pnlMicros := pnlPerShareMicros*qtyUnits + pnlPerShareMicros*qtyRemainder/1_000_000
236+
if isBond {
237+
pnlMicros /= 100
238+
}
239+
holding.UnrealizedPnLUSD = moneypb.MoneyValueToString(moneypb.MoneyFromMicros("USD", pnlMicros))
240+
}
189241
}
190242
// Merge symbol classification from config.
191243
if symbolConfig, ok := config.SymbolConfigs[symbol]; ok {

0 commit comments

Comments
 (0)