fix(价格展示): 修复模板渲染时极小价格被折叠为 0.00 的问题
Motivation: 此前的修复仅覆盖直接拼接文案的场景,跟单等基于模板渲染的推送场景中,%.2f 格式化仍会把 PEPE 等 Meme 币的极小价格(如 0.00000059)折叠为 0.00,导致交易通知丢失真实价格、误导用户。 Changes: * 抽取共享的价格显示逻辑到统一显示层,两个订阅者改为复用,移除重复实现 * 新增 Decimal 类型,渲染时以副本方式包装模板数据,使模板内 printf 风格格式化保留极小价格精度且不出现科学计数法,同时不修改调用方原始数据 * 渲染引擎空值判断改用反射实现,覆盖全部整型、无符号整型与浮点类型 * 补充转换到渲染的端到端回归测试,覆盖极小价格精度、科学计数法与数据不可变性
This commit is contained in:
@@ -33,7 +33,7 @@ func TestRendererLineOmitsEmpty(t *testing.T) {
|
||||
"symbol": "ICP",
|
||||
"price": 2.273,
|
||||
"totalAvgPx": "",
|
||||
"stopLossPrice": 0,
|
||||
"stopLossPrice": float64(0),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -122,8 +122,12 @@ func TestRendererCaseDefault(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// copyTradeTmpl is the production 跟单策略 template. It formats prices with
|
||||
// printf "%.2f", which must not collapse meme-coin prices like PEPE to 0.00.
|
||||
const copyTradeTmpl = "{{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}}"
|
||||
|
||||
func TestCopyTradeTemplate(t *testing.T) {
|
||||
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}}"
|
||||
tmpl := copyTradeTmpl
|
||||
r := NewRenderer()
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -169,6 +173,21 @@ func TestCopyTradeTemplate(t *testing.T) {
|
||||
},
|
||||
want: []string{"空单加仓", "加仓价格: 1933.05", "加仓数量: 3.80", "杠杆: 100x", "策略: B龙策略", "推送时间: 2026.08.10 06:14:28"},
|
||||
},
|
||||
{
|
||||
name: "pepe-open-keeps-tiny-price",
|
||||
data: map[string]interface{}{
|
||||
"side": "SHORT", "action": "OPEN", "symbol": "PEPEUSDT",
|
||||
"price": 0.00000059, "quantity": 1000000000.0, "avgPrice": 0.00000059,
|
||||
"leverage": 100, "strategyCode": "BLONG", "pushedAt": "2026.08.22 10:04:29",
|
||||
},
|
||||
want: []string{
|
||||
"空单开仓", "交易品种: PEPEUSDT",
|
||||
"开仓价格: 0.00000059", "开仓数量: 1000000000.00",
|
||||
"平均单价: 0.00000059", "杠杆: 100x",
|
||||
"策略: B龙策略", "推送时间: 2026.08.22 10:04:29",
|
||||
},
|
||||
not: []string{"开仓价格: 0.00\n", "平均单价: 0.00\n"},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -190,3 +209,30 @@ func TestCopyTradeTemplate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRendererTinyPriceNotScientificOrRounded(t *testing.T) {
|
||||
r := NewRenderer()
|
||||
data := map[string]interface{}{"price": 0.00000059, "stopLossPrice": 0.00000055}
|
||||
out, err := r.Render(`价格: {{.price}}
|
||||
{{line "止损价" .stopLossPrice}}`, data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(out, "e-") || strings.Contains(out, "E-") {
|
||||
t.Fatalf("tiny price lost precision:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "价格: 0.00000059") || !strings.Contains(out, "止损价:0.00000055") {
|
||||
t.Fatalf("out=%q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRendererDoesNotMutateData(t *testing.T) {
|
||||
r := NewRenderer()
|
||||
data := map[string]interface{}{"price": 0.00000059}
|
||||
if _, err := r.Render(`{{printf "%.2f" .price}}`, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := data["price"].(float64); !ok {
|
||||
t.Fatalf("render mutated caller data: %T", data["price"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user