Files
aiaa-notification-server/internal/engine/renderer_test.go
T
ryan 8e6a1332f0 fix(交易信号): 修复未追踪仓位时平均单价渲染为 %!f(<nil>) 的问题
- 本地无均价记录时回退使用上游 totalAvgPx
- 跟单模板在无均价时不再输出平均单价行
2026-10-10 00:48:35 +08:00

239 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package engine
import (
"strings"
"testing"
)
func TestRenderer(t *testing.T) {
r := NewRenderer()
tmpl := "🚀 {{.symbol}} 开仓通知\n价格: {{.price}}"
data := map[string]interface{}{"symbol": "BTC", "price": 65000}
result, err := r.Render(tmpl, data)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(result, "BTC") || !strings.Contains(result, "65000") {
t.Errorf("unexpected output: %s", result)
}
}
func TestRendererMissingKeyEmpty(t *testing.T) {
r := NewRenderer()
_, err := r.Render("x{{.nonexistent}}y", map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
}
func TestRendererLineOmitsEmpty(t *testing.T) {
r := NewRenderer()
tmpl := `{{line "币种" .symbol}}{{line "平均价" .totalAvgPx}}{{line "价格" .price}}{{line "止损价" .stopLossPrice}}`
out, err := r.Render(tmpl, map[string]interface{}{
"symbol": "ICP",
"price": 2.273,
"totalAvgPx": "",
"stopLossPrice": float64(0),
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "币种:ICP") || !strings.Contains(out, "价格:2.273") {
t.Fatalf("out=%q", out)
}
if strings.Contains(out, "平均价") || strings.Contains(out, "止损价") {
t.Fatalf("empty lines should be omitted, out=%q", out)
}
}
func TestRendererLineLabelAlreadyHasColon(t *testing.T) {
r := NewRenderer()
out, err := r.Render(`{{line "平均价:" .totalAvgPx}}`, map[string]interface{}{"totalAvgPx": 1029})
if err != nil {
t.Fatal(err)
}
if strings.Count(out, ":") != 1 || !strings.Contains(out, "平均价:1029") {
t.Fatalf("out=%q", out)
}
}
func TestRendererLineMissingKey(t *testing.T) {
r := NewRenderer()
out, err := r.Render(`{{line "平均价" .totalAvgPx}}{{line "币种" .symbol}}`, map[string]interface{}{"symbol": "ICP"})
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "平均价") || !strings.Contains(out, "币种:ICP") {
t.Fatalf("out=%q", out)
}
}
func TestRendererCaseSwitch(t *testing.T) {
r := NewRenderer()
tmpl := `{{case .action "OPEN" "开仓" "CLOSE" "平仓" "GAIN" "止盈"}}`
out, err := r.Render(tmpl, map[string]interface{}{"action": "CLOSE"})
if err != nil {
t.Fatal(err)
}
if out != "平仓" {
t.Fatalf("action CLOSE: %q", out)
}
out, err = r.Render(tmpl, map[string]interface{}{"action": "open"})
if err != nil {
t.Fatal(err)
}
if out != "开仓" {
t.Fatalf("action open: %q", out)
}
}
func TestRendererCaseEventSuffix(t *testing.T) {
r := NewRenderer()
tmpl := `{{case .event ".open" "开仓" ".close" "平仓"}}`
out, err := r.Render(tmpl, map[string]interface{}{"event": "trade.close"})
if err != nil {
t.Fatal(err)
}
if out != "平仓" {
t.Fatalf("event trade.close: %q", out)
}
}
func TestRendererReplace(t *testing.T) {
r := NewRenderer()
out, err := r.Render(`{{replace .rawMessage "Time:" "推送时间:"}}`, map[string]interface{}{
"rawMessage": "激进版AI 1.0\n市价开空\nTime: 2026.08.17 14:46:04",
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "推送时间: 2026.08.17 14:46:04") || strings.Contains(out, "Time:") {
t.Fatalf("out=%q", out)
}
}
func TestRendererCaseDefault(t *testing.T) {
r := NewRenderer()
out, err := r.Render(`{{case .action "OPEN" "开仓" "未知"}}`, map[string]interface{}{"action": "HOLD"})
if err != nil {
t.Fatal(err)
}
if out != "未知" {
t.Fatalf("got %q", out)
}
}
// 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{{if .avgPrice}}平均单价: {{printf \"%.2f\" .avgPrice}}\n{{end}}{{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 := copyTradeTmpl
r := NewRenderer()
cases := []struct {
name string
data map[string]interface{}
want []string
not []string
}{
{
name: "open",
data: map[string]interface{}{
"side": "SHORT", "action": "OPEN", "symbol": "ETHUSDT",
"price": 1898.76, "quantity": 2.0, "avgPrice": 1898.76,
"leverage": 100, "strategyCode": "BLONG", "pushedAt": "2026.08.13 13:15:42",
},
want: []string{"空单开仓", "交易品种: ETH", "开仓价格: 1898.76", "开仓数量: 2.00", "平均单价: 1898.76", "杠杆: 100x", "策略: B龙策略", "推送时间: 2026.08.13 13:15:42"},
},
{
name: "close",
data: map[string]interface{}{
"side": "SHORT", "action": "CLOSE", "symbol": "ETHUSDT",
"price": 1883.35, "quantity": 2.0, "avgPrice": 1898.76,
"leverage": 100, "strategyCode": "BLONG", "pushedAt": "2026.08.13 17:14:31",
},
want: []string{"空单平仓", "平仓价格: 1883.35", "平仓数量: 2.00", "策略: B龙策略", "推送时间: 2026.08.13 17:14:31"},
not: []string{"杠杆:"},
},
{
name: "reduce",
data: map[string]interface{}{
"side": "SHORT", "action": "REDUCE", "symbol": "ETHUSDT",
"price": 1889.43, "quantity": 3.1, "avgPrice": 1899.03,
"strategyCode": "BLONG", "pushedAt": "2026.08.12 22:05:02",
},
want: []string{"空单减仓", "减仓价格: 1889.43", "减仓数量: 3.10", "平均单价: 1899.03", "策略: B龙策略"},
not: []string{"杠杆:"},
},
{
name: "add",
data: map[string]interface{}{
"side": "SHORT", "action": "ADD", "symbol": "ETHUSDT",
"price": 1933.05, "quantity": 3.8, "avgPrice": 1933.05,
"leverage": 100, "strategyCode": "BLONG", "pushedAt": "2026.08.10 06:14:28",
},
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) {
out, err := r.Render(tmpl, tc.data)
if err != nil {
t.Fatal(err)
}
t.Logf("\n%s", out)
for _, w := range tc.want {
if !strings.Contains(out, w) {
t.Errorf("missing %q in\n%s", w, out)
}
}
for _, n := range tc.not {
if strings.Contains(out, n) {
t.Errorf("unexpected %q in\n%s", n, out)
}
}
})
}
}
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"])
}
}