fix(stock): note currency in portfolio title

This commit is contained in:
tiennm99 committed 2026-07-22 10:59:40 +07:00
1 parent cc7dd4aa10
commit 7487d21499
4 files changed
+68 -18

No files matched your search

@@ -0,0 +1,32 @@
---
date: 2026-07-22
component: internal/modules/stock
status: completed
---
# Stock Portfolio Currency Title
## Context
The stock portfolio repeated `VND` in every monetary table cell.
## What Happened
The renderer now declares `VND` once in the `Stock Portfolio (VND)` title,
labels the balance row `Cash`, and uses suffix-free monetary values in the
portfolio table. Standalone stock replies retain their explicit `VND` suffix.
## Reflection
A portfolio-specific display helper kept this presentation change from altering
the exported formatters used by trades, prices, and dividends.
## Decisions
- Keep command, storage, and exported formatting contracts unchanged.
- Cover the title, cash label, suffix-free values, and single currency marker in
the portfolio regression test.
## Next
No follow-up required. Focused and full tests, vet, and lint passed.
+15 -2
View File
@@ -12,6 +12,12 @@ import (
// and a "VND" suffix (e.g. 15000000 -> "15.000.000 VND"). Manual to avoid
// locale-dependent formatting.
func FormatVND(n float64) string {
return formatVNDNumber(n) + " VND"
}
// formatVNDNumber renders a VND amount without its currency suffix. Portfolio
// tables use this because their title declares the currency once.
func formatVNDNumber(n float64) string {
rounded := int64(math.Round(n))
abs := strconv.FormatInt(absInt64(rounded), 10)
var sb strings.Builder
@@ -24,7 +30,6 @@ func FormatVND(n float64) string {
}
sb.WriteByte(abs[i])
}
sb.WriteString(" VND")
return sb.String()
}
@@ -60,6 +65,14 @@ func formatShareQuantity(n int64) string {
// "+1.234 VND (+12.34%)" or "-500.000 VND (-5.00%)". When invested is zero
// the percentage is reported as 0.00 to avoid division-by-zero.
func FormatPnL(currentValue, invested float64) string {
return formatPnL(currentValue, invested, FormatVND)
}
func formatPortfolioPnL(currentValue, invested float64) string {
return formatPnL(currentValue, invested, formatVNDNumber)
}
func formatPnL(currentValue, invested float64, formatAmount func(float64) string) string {
diff := currentValue - invested
pct := 0.0
if invested > 0 {
@@ -69,7 +82,7 @@ func FormatPnL(currentValue, invested float64) string {
if diff >= 0 {
sign = "+"
}
return sign + FormatVND(diff) + " (" + sign + strconv.FormatFloat(pct, 'f', 2, 64) + "%)"
return sign + formatAmount(diff) + " (" + sign + strconv.FormatFloat(pct, 'f', 2, 64) + "%)"
}
func absInt64(n int64) int64 {
+12 -12
View File
@@ -510,7 +510,7 @@ func (s *state) handleStats(ctx context.Context, b *bot.Bot, update *models.Upda
average := basis / float64(h.qty)
if !isPositiveFiniteCost(price) {
missingPrice = true
positions = append(positions, []string{h.symbol, FormatStock(float64(h.qty)), FormatVND(average), "N/A", "N/A", "N/A"})
positions = append(positions, []string{h.symbol, FormatStock(float64(h.qty)), formatVNDNumber(average), "N/A", "N/A", "N/A"})
continue
}
val := float64(h.qty) * price
@@ -521,28 +521,28 @@ func (s *state) handleStats(ctx context.Context, b *bot.Bot, update *models.Upda
}
totalValue += val
totalBasis += basis
positions = append(positions, []string{h.symbol, FormatStock(float64(h.qty)), FormatVND(average), FormatVND(price), FormatVND(val), FormatPnL(val, basis)})
positions = append(positions, []string{h.symbol, FormatStock(float64(h.qty)), formatVNDNumber(average), formatVNDNumber(price), formatVNDNumber(val), formatPortfolioPnL(val, basis)})
}
}
var summary [][]string
if missingPrice {
summary = [][]string{
{"VND", FormatVND(p.VND)},
{"Priced value (partial)", FormatVND(totalValue)},
{"Unrealized P&L (priced)", FormatPnL(totalValue-p.VND, totalBasis)},
{"Invested", FormatVND(p.Meta.Invested)},
{"Cash", formatVNDNumber(p.VND)},
{"Priced value (partial)", formatVNDNumber(totalValue)},
{"Unrealized P&L (priced)", formatPortfolioPnL(totalValue-p.VND, totalBasis)},
{"Invested", formatVNDNumber(p.Meta.Invested)},
{"Account P&L", "Unavailable"},
}
} else {
summary = [][]string{
{"VND", FormatVND(p.VND)},
{"Total value", FormatVND(totalValue)},
{"Invested", FormatVND(p.Meta.Invested)},
{"Unrealized P&L", FormatPnL(totalValue-p.VND, totalBasis)},
{"Account P&L", FormatPnL(totalValue, p.Meta.Invested)},
{"Cash", formatVNDNumber(p.VND)},
{"Total value", formatVNDNumber(totalValue)},
{"Invested", formatVNDNumber(p.Meta.Invested)},
{"Unrealized P&L", formatPortfolioPnL(totalValue-p.VND, totalBasis)},
{"Account P&L", formatPortfolioPnL(totalValue, p.Meta.Invested)},
}
}
if err := chathelper.ReplyHTML(ctx, b, update.Message, portfolioTableReply("Stock Portfolio", positions, summary)); err != nil {
if err := chathelper.ReplyHTML(ctx, b, update.Message, portfolioTableReply("Stock Portfolio (VND)", positions, summary)); err != nil {
return err
}
return s.notifyDividendEvents(ctx, b, update.Message, userID, p, checkedThrough)
+9 -4
View File
@@ -67,16 +67,18 @@ func TestHandleStats_UsesSSIBatchPrices(t *testing.T) {
text := rb.LastSent().Text()
for _, want := range []string{
"Stock Portfolio (VND)",
"<pre>",
"Ticker",
"MWG",
"126.000.000 VND",
"126.000.000",
"Cash",
"Total value",
"530.335.000 VND",
"530.335.000",
"Unrealized P&amp;L",
"+85.000.000 VND (+19.19%)",
"+85.000.000 (+19.19%)",
"Account P&amp;L",
"-469.665.000 VND (-46.97%)",
"-469.665.000 (-46.97%)",
} {
if !strings.Contains(text, want) {
t.Fatalf("stats missing %q in:\n%s", want, text)
@@ -85,6 +87,9 @@ func TestHandleStats_UsesSSIBatchPrices(t *testing.T) {
if strings.Contains(text, "N/A") {
t.Fatalf("stats rendered missing prices:\n%s", text)
}
if strings.Count(text, "VND") != 1 {
t.Fatalf("stats should declare VND only in the title:\n%s", text)
}
}
func TestStockPortfolioReplyStaysWithinTelegramBudget(t *testing.T) {