@@ -58,14 +58,17 @@ type HoldingOverview struct {
5858 Type string `json:"type,omitempty"`
5959 // Sector is the user-defined sector classification (e.g., "TECH").
6060 Sector string `json:"sector,omitempty"`
61+ // Geo is the user-defined geographic classification (e.g., "US", "INTL").
62+ Geo string `json:"geo,omitempty"`
6163}
6264
6365// HoldingsOverviewHeaders returns the column headers for table/CSV output.
6466func HoldingsOverviewHeaders () []string {
65- return []string {"SYMBOL" , "CURRENCY" , "LAST PRICE" , "AVG PRICE" , "LAST USD" , "AVG USD" , "MKT VAL USD" , "UNRLZD P&L USD" , "POSITION" , "CATEGORY" , "TYPE" , "SECTOR" }
67+ return []string {"SYMBOL" , "CURRENCY" , "LAST PRICE" , "AVG PRICE" , "LAST USD" , "AVG USD" , "MKT VAL USD" , "UNRLZD P&L USD" , "POSITION" , "CATEGORY" , "TYPE" , "SECTOR" , "GEO" }
6668}
6769
68- // HoldingOverviewToRow converts a HoldingOverview to a string slice for table/CSV output.
70+ // HoldingOverviewToRow converts a HoldingOverview to a string slice for CSV output.
71+ // USD values are kept as raw decimals for machine-readable output.
6972func HoldingOverviewToRow (h * HoldingOverview ) []string {
7073 return []string {
7174 h .Symbol ,
@@ -80,11 +83,32 @@ func HoldingOverviewToRow(h *HoldingOverview) []string {
8083 h .Category ,
8184 h .Type ,
8285 h .Sector ,
86+ h .Geo ,
87+ }
88+ }
89+
90+ // HoldingOverviewToTableRow converts a HoldingOverview to a string slice for
91+ // table display. USD values are rounded to cents with $ prefix and comma separators.
92+ func HoldingOverviewToTableRow (h * HoldingOverview ) []string {
93+ return []string {
94+ h .Symbol ,
95+ h .Currency ,
96+ h .LastPrice ,
97+ h .AveragePrice ,
98+ formatUSD (h .LastPriceUSD ),
99+ formatUSD (h .AveragePriceUSD ),
100+ formatUSD (h .MarketValueUSD ),
101+ formatUSD (h .UnrealizedPnLUSD ),
102+ mathpb .ToString (h .Position ),
103+ h .Category ,
104+ h .Type ,
105+ h .Sector ,
106+ h .Geo ,
83107 }
84108}
85109
86110// ComputeTotals sums the MarketValueUSD and UnrealizedPnLUSD across all holdings.
87- // Returns formatted strings for the totals row.
111+ // Returns formatted USD strings for the totals row (rounded to cents with $ prefix) .
88112func ComputeTotals (holdings []* HoldingOverview ) (string , string ) {
89113 var totalMktValMicros , totalPnLMicros int64
90114 for _ , h := range holdings {
@@ -99,8 +123,8 @@ func ComputeTotals(holdings []*HoldingOverview) (string, string) {
99123 }
100124 }
101125 }
102- return moneypb .MoneyValueToString (moneypb .MoneyFromMicros ("USD" , totalMktValMicros )),
103- moneypb .MoneyValueToString (moneypb .MoneyFromMicros ("USD" , totalPnLMicros ))
126+ return formatUSD ( moneypb .MoneyValueToString (moneypb .MoneyFromMicros ("USD" , totalMktValMicros ) )),
127+ formatUSD ( moneypb .MoneyValueToString (moneypb .MoneyFromMicros ("USD" , totalPnLMicros ) ))
104128}
105129
106130// GetHoldingsOverview computes the holdings overview from trade data using FIFO,
@@ -244,6 +268,7 @@ func GetHoldingsOverview(
244268 holding .Category = symbolConfig .Category
245269 holding .Type = symbolConfig .Type
246270 holding .Sector = symbolConfig .Sector
271+ holding .Geo = symbolConfig .Geo
247272 }
248273 holdings = append (holdings , holding )
249274 }
@@ -258,3 +283,24 @@ func GetHoldingsOverview(
258283 PositionDiscrepancies : discrepancies ,
259284 }, nil
260285}
286+
287+ // *** PRIVATE ***
288+
289+ // formatUSD formats a raw decimal string as a USD value with $ prefix,
290+ // rounded to cents with comma separators (e.g., "$1,234.56", "-$789.01").
291+ // Returns empty string for empty input.
292+ func formatUSD (value string ) string {
293+ if value == "" {
294+ return ""
295+ }
296+ decimal , err := mathpb .NewDecimal (value )
297+ if err != nil {
298+ return value
299+ }
300+ formatted := mathpb .Format (decimal , 2 )
301+ // Prepend $ after any negative sign.
302+ if len (formatted ) > 0 && formatted [0 ] == '-' {
303+ return "-$" + formatted [1 :]
304+ }
305+ return "$" + formatted
306+ }
0 commit comments