mirror of
https://github.com/tiennm99/tiennm99bot.git
synced 2026-10-15 00:17:53 +00:00
fix(stock): note currency in portfolio title
This commit is contained in:
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.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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&L",
|
||||
"+85.000.000 VND (+19.19%)",
|
||||
"+85.000.000 (+19.19%)",
|
||||
"Account P&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) {
|
||||
|
||||
Reference in new issue
Block a user