fix(价格展示): 修复模板渲染时极小价格被折叠为 0.00 的问题

Motivation:
此前的修复仅覆盖直接拼接文案的场景,跟单等基于模板渲染的推送场景中,%.2f 格式化仍会把 PEPE 等 Meme 币的极小价格(如 0.00000059)折叠为 0.00,导致交易通知丢失真实价格、误导用户。

Changes:

* 抽取共享的价格显示逻辑到统一显示层,两个订阅者改为复用,移除重复实现
* 新增 Decimal 类型,渲染时以副本方式包装模板数据,使模板内 printf 风格格式化保留极小价格精度且不出现科学计数法,同时不修改调用方原始数据
* 渲染引擎空值判断改用反射实现,覆盖全部整型、无符号整型与浮点类型
* 补充转换到渲染的端到端回归测试,覆盖极小价格精度、科学计数法与数据不可变性
This commit is contained in:
2026-08-23 00:12:41 +08:00
parent c7e154d7cc
commit 6f5aaa9f85
6 changed files with 135 additions and 60 deletions
@@ -6,6 +6,7 @@ import (
"testing"
"aiaa-notification-service/internal/config"
"aiaa-notification-service/internal/engine"
)
func TestConvertOpen(t *testing.T) {
@@ -97,3 +98,43 @@ func TestConvertRawMessageWithoutAction(t *testing.T) {
t.Fatalf("data=%v", data)
}
}
func TestConvertThenRenderCopyTradeTinyPEPE(t *testing.T) {
lev := 100
c := NewConverter(map[string]config.StrategyOverride{
"BLONG": {QuantityMultipliers: config.QuantityMultipliers{Open: 100}, Leverage: &lev},
})
_, data, err := c.Convert([]byte(`{
"signalId":"s1","strategyCode":"BLONG","symbol":"PEPEUSDT",
"side":"SHORT","action":"OPEN","quantity":10000000,"price":0.00000059,
"leverage":10,"eventTime":"2026-08-22T02:04:29Z"
}`))
if err != nil {
t.Fatal(err)
}
if p, ok := data["price"].(float64); !ok || p != 0.00000059 {
t.Fatalf("convert price=%v (%T), want float64 0.00000059", data["price"], data["price"])
}
data["pushedAt"] = "2026.08.22 10:04:29"
const tmpl = "{{case .side \"LONG\" \"多单\" \"SHORT\" \"空单\"}}{{case .action \"OPEN\" \"开仓\" \"ADD\" \"加仓\" \"CLOSE\" \"平仓\" \"REDUCE\" \"减仓\"}}\n交易品种: {{case .symbol \"ETHUSDT\" \"ETH\" \"BTCUSDT\" \"BTC\" \"SOLUSDT\" \"SOL\" \"BNBUSDT\" \"BNB\" .symbol}}\n{{case .action \"OPEN\" \"开仓价格\" \"ADD\" \"加仓价格\" \"CLOSE\" \"平仓价格\" \"REDUCE\" \"减仓价格\"}}: {{printf \"%.2f\" .price}}\n{{case .action \"OPEN\" \"开仓数量\" \"ADD\" \"加仓数量\" \"CLOSE\" \"平仓数量\" \"REDUCE\" \"减仓数量\"}}: {{printf \"%.2f\" .quantity}}\n平均单价: {{printf \"%.2f\" .avgPrice}}\n{{if or (eq .action \"OPEN\") (eq .action \"ADD\")}}{{if .leverage}}杠杆: {{.leverage}}x\n{{end}}{{end}}策略: {{case .strategyCode \"BLONG\" \"B龙策略\" .strategyCode}}\n推送时间: {{.pushedAt}}"
out, err := engine.NewRenderer().Render(tmpl, data)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"空单开仓",
"交易品种: PEPEUSDT",
"开仓价格: 0.00000059",
"开仓数量: 1000000000.00",
"平均单价: 0.00000059",
"杠杆: 100x",
} {
if !strings.Contains(out, want) {
t.Errorf("missing %q in\n%s", want, out)
}
}
if strings.Contains(out, "开仓价格: 0.00\n") || strings.Contains(out, "平均单价: 0.00\n") {
t.Errorf("tiny price collapsed to 0.00:\n%s", out)
}
}
+13 -27
View File
@@ -2,10 +2,9 @@ package tradesignal
import (
"fmt"
"math"
"strconv"
"strings"
"aiaa-notification-service/internal/display"
"aiaa-notification-service/internal/tz"
)
@@ -31,7 +30,7 @@ func Format(signal *Signal, opts ...FormatOptions) string {
action := strings.ToUpper(signal.Action)
switch action {
case "OPEN":
lines = append(lines, fmt.Sprintf("开仓价格: %s", formatPrice(signal.Price)))
lines = append(lines, fmt.Sprintf("开仓价格: %s", display.FormatPrice(signal.Price)))
if line := sizeLine("OPEN", signal.Quantity, signal.AmountMarginRatio); line != "" {
lines = append(lines, line)
}
@@ -40,23 +39,23 @@ func Format(signal *Signal, opts ...FormatOptions) string {
lines = append(lines, fmt.Sprintf("杠杆: %dx", signal.Leverage))
}
if signal.TakeProfitPrice != nil {
lines = append(lines, fmt.Sprintf("止盈价格: %s", formatPrice(*signal.TakeProfitPrice)))
lines = append(lines, fmt.Sprintf("止盈价格: %s", display.FormatPrice(*signal.TakeProfitPrice)))
}
if signal.StopLossPrice != nil {
lines = append(lines, fmt.Sprintf("止损价格: %s", formatPrice(*signal.StopLossPrice)))
lines = append(lines, fmt.Sprintf("止损价格: %s", display.FormatPrice(*signal.StopLossPrice)))
}
case "CLOSE":
lines = append(lines, fmt.Sprintf("平仓价格: %s", formatPrice(signal.Price)))
lines = append(lines, fmt.Sprintf("平仓价格: %s", display.FormatPrice(signal.Price)))
lines = append(lines, closeSizeLine(signal.Quantity, signal.PosMarginRatio))
lines = appendAvgPrice(lines, opt.AvgPrice)
if signal.PnL != nil {
lines = append(lines, fmt.Sprintf("平仓盈亏: %s", formatPrice(*signal.PnL)))
lines = append(lines, fmt.Sprintf("平仓盈亏: %s", display.FormatPrice(*signal.PnL)))
}
if signal.AccountBalance != nil {
lines = append(lines, fmt.Sprintf("账户余额:%s", formatPrice(*signal.AccountBalance)))
lines = append(lines, fmt.Sprintf("账户余额:%s", display.FormatPrice(*signal.AccountBalance)))
}
case "ADD":
lines = append(lines, fmt.Sprintf("加仓价格: %s", formatPrice(signal.Price)))
lines = append(lines, fmt.Sprintf("加仓价格: %s", display.FormatPrice(signal.Price)))
if line := sizeLine("ADD", signal.Quantity, signal.AmountMarginRatio); line != "" {
lines = append(lines, line)
}
@@ -65,19 +64,19 @@ func Format(signal *Signal, opts ...FormatOptions) string {
lines = append(lines, fmt.Sprintf("杠杆: %dx", signal.Leverage))
}
case "REDUCE":
lines = append(lines, fmt.Sprintf("减仓价格: %s", formatPrice(signal.Price)))
lines = append(lines, fmt.Sprintf("减仓价格: %s", display.FormatPrice(signal.Price)))
if line := sizeLine("REDUCE", signal.Quantity, signal.PosMarginRatio); line != "" {
lines = append(lines, line)
}
lines = appendAvgPrice(lines, opt.AvgPrice)
if signal.PnL != nil {
lines = append(lines, fmt.Sprintf("减仓盈亏: %s", formatPrice(*signal.PnL)))
lines = append(lines, fmt.Sprintf("减仓盈亏: %s", display.FormatPrice(*signal.PnL)))
}
if signal.AccountBalance != nil {
lines = append(lines, fmt.Sprintf("账户余额:%s", formatPrice(*signal.AccountBalance)))
lines = append(lines, fmt.Sprintf("账户余额:%s", display.FormatPrice(*signal.AccountBalance)))
}
default:
lines = append(lines, fmt.Sprintf("价格: %s", formatPrice(signal.Price)))
lines = append(lines, fmt.Sprintf("价格: %s", display.FormatPrice(signal.Price)))
if line := sizeLine("", signal.Quantity, signal.AmountMarginRatio); line != "" {
lines = append(lines, line)
}
@@ -129,7 +128,7 @@ func appendAvgPrice(lines []string, avgPrice *float64) []string {
if avgPrice == nil || *avgPrice <= 0 {
return lines
}
return append(lines, fmt.Sprintf("平均单价: %s", formatPrice(*avgPrice)))
return append(lines, fmt.Sprintf("平均单价: %s", display.FormatPrice(*avgPrice)))
}
func closeSizeLine(quantity, posMarginRatio *float64) string {
@@ -190,19 +189,6 @@ func formatPercent(ratio float64) string {
return fmt.Sprintf("%.2f%%", ratio*100)
}
// formatPrice renders a price for display. Values >= 1 keep two decimals for
// readability; smaller values use the shortest exact representation so tiny
// prices like 0.00000059 are not collapsed to 0.00.
func formatPrice(v float64) string {
if v == 0 {
return "0.00"
}
if math.Abs(v) >= 1 {
return strconv.FormatFloat(v, 'f', 2, 64)
}
return strconv.FormatFloat(v, 'f', -1, 64)
}
func trimQuote(symbol string) string {
symbol = strings.ToUpper(symbol)
for _, suffix := range []string{"USDT", "USDC", "BUSD", "USD"} {