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

Changes:

* 抽取共享的价格显示逻辑到统一显示层,两个订阅者改为复用,移除重复实现
* 新增 Decimal 类型,渲染时以副本方式包装模板数据,使模板内 printf 风格格式化保留极小价格精度且不出现科学计数法,同时不修改调用方原始数据
* 渲染引擎空值判断改用反射实现,覆盖全部整型、无符号整型与浮点类型
* 补充转换到渲染的端到端回归测试,覆盖极小价格精度、科学计数法与数据不可变性
2026-08-23 00:12:41 +08:00

667 lines
21 KiB
Go
Raw Permalink 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 cryptostrategy
import (
"strings"
"testing"
"aiaa-notification-service/internal/engine"
)
const sampleBody = `{
"eventType": "SIGNAL_RECEIVED",
"correlationId": "0_0_0",
"symbol": "QNT",
"direction": "LONG",
"payload": "{\"strategyCode\":\"ai-crypto-signals\",\"period\":\"1h\",\"currency\":\"QNT\",\"isSale\":false,\"isClose\":false,\"isGain\":false,\"gainTarget\":5,\"price\":58.23,\"lossPrice\":57.82,\"gainPrices\":\"58.435,58.64,58.845,59.05,59.255\",\"remark\":\"{\\\"orderId\\\":\\\"jeJY8l5YnYwfJbmj6zb4\\\"}\",\"totalGainTarget\":5,\"leverage\":43}",
"eventTime": 1786802842899
}`
func TestConvertParsesNestedPayload(t *testing.T) {
event, data, err := Convert([]byte(sampleBody))
if err != nil {
t.Fatal(err)
}
if event != "trade.open" {
t.Fatalf("event=%q", event)
}
if data["symbol"] != "QNT" || data["strategyCode"] != "ai-crypto-signals" {
t.Fatalf("data=%v", data)
}
if data["period"] != "1h" || data["direction"] != "LONG" {
t.Fatalf("data=%v", data)
}
if data["orderId"] != "jeJY8l5YnYwfJbmj6zb4" {
t.Fatalf("orderId=%v", data["orderId"])
}
formatted, _ := data["formatted"].(string)
for _, want := range []string{
"多单开仓",
"交易品种: QNT",
"周期: 1h",
"开仓价格: 58.23",
"止损价格: 57.82",
"止盈价格: 58.435, 58.64, 58.845, 59.05, 59.255",
"杠杆: 43x",
"策略: ai-crypto-signals",
} {
if !strings.Contains(formatted, want) {
t.Fatalf("missing %q in\n%s", want, formatted)
}
}
}
func TestConvertPushedAtIsUTC8(t *testing.T) {
body := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"CRV","direction":"LONG",
"payload":"{\"strategyCode\":\"ai-crypto-signals\",\"isClose\":true,\"price\":0.2528}",
"eventTime":1786899539730
}`)
_, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
if data["pushedAt"] != "2026-08-17 00:58:59" {
t.Fatalf("pushedAt=%v want UTC+8 2026-08-17 00:58:59", data["pushedAt"])
}
formatted, _ := data["formatted"].(string)
if !strings.Contains(formatted, "Time: 2026.08.17 00:58:59") {
t.Fatalf("formatted=%s", formatted)
}
}
func TestConvertCloseFlag(t *testing.T) {
body := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"BTC","direction":"SHORT",
"payload":"{\"isClose\":true,\"price\":64000,\"strategyCode\":\"x\",\"period\":\"4h\"}",
"eventTime":1786802842899
}`)
event, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
if event != "trade.close" {
t.Fatalf("event=%q", event)
}
formatted, _ := data["formatted"].(string)
if !strings.Contains(formatted, "空单平仓") {
t.Fatalf("%s", formatted)
}
}
func TestConvertFlatOneLayer(t *testing.T) {
body := []byte(`{
"eventType":"SIGNAL_RECEIVED",
"correlationId":"0_0_0",
"symbol":"QNT",
"direction":"LONG",
"strategyCode":"ai-crypto-signals",
"period":"1h",
"currency":"QNT",
"isSale":false,
"isClose":false,
"isGain":false,
"gainTarget":5,
"price":58.23,
"lossPrice":57.82,
"gainPrices":"58.435,58.64",
"remark":"{\"orderId\":\"jeJY8l5YnYwfJbmj6zb4\"}",
"leverage":43,
"eventTime":1786802842899
}`)
event, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
if event != "trade.open" {
t.Fatalf("event=%q", event)
}
if data["strategyCode"] != "ai-crypto-signals" || data["price"] != 58.23 {
t.Fatalf("data=%v", data)
}
if data["orderId"] != "jeJY8l5YnYwfJbmj6zb4" {
t.Fatalf("orderId=%v", data["orderId"])
}
formatted, _ := data["formatted"].(string)
if !strings.Contains(formatted, "多单开仓") || !strings.Contains(formatted, "开仓价格: 58.23") {
t.Fatalf("%s", formatted)
}
}
func TestConvertPayloadObject(t *testing.T) {
body := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"ETH","direction":"SHORT",
"payload":{"isClose":true,"price":3200,"strategyCode":"x","period":"1h"},
"eventTime":1786802842899
}`)
event, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
if event != "trade.close" {
t.Fatalf("event=%q", event)
}
if data["price"] != float64(3200) {
t.Fatalf("data=%v", data)
}
}
func TestConvertRendersSharedSignalTemplate(t *testing.T) {
body := []byte(`{
"eventType": "SIGNAL_RECEIVED",
"correlationId": "0_0_0",
"symbol": "ICP",
"direction": "LONG",
"payload": "{\"strategyCode\":\"ai-crypto-signals\",\"period\":\"1h\",\"currency\":\"ICP\",\"isSale\":false,\"isClose\":true,\"isGain\":true,\"gainTarget\":1,\"price\":2.273,\"remark\":\"{\\\"orderId\\\":\\\"uJs3zQI8IaRV16n8NERq\\\",\\\"revenue\\\":\\\"14.1088%\\\"}\",\"totalGainTarget\":5}",
"eventTime": 1786808929754
}`)
_, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
for _, key := range []string{"totalAvgPx", "takeProfitPrice", "stopLossPrice", "side", "action", "symbol", "period", "price"} {
if _, ok := data[key]; !ok {
t.Fatalf("missing template key %q in %v", key, data)
}
}
if data["orderId"] != "uJs3zQI8IaRV16n8NERq" || data["revenue"] != "14.1088%" {
t.Fatalf("remark=%v", data)
}
tmpl := "### {{.symbol}} {{.action}}\n币种:{{.symbol}}\n周期:{{.period}}\n方向:{{.side}}\n价格:{{.price}}\n平均价:{{.totalAvgPx}}\n止盈价:{{.takeProfitPrice}}\n止损价:{{.stopLossPrice}}"
out, err := engine.NewRenderer().Render(tmpl, data)
if err != nil {
t.Fatalf("render: %v", err)
}
if !strings.Contains(out, "ICP") || !strings.Contains(out, "GAIN") {
t.Fatalf("out=%s", out)
}
}
func TestConvertHLSSParsesGainPricesAndOpenPrice2(t *testing.T) {
body := []byte(`{
"eventType": "SIGNAL_RECEIVED",
"correlationId": "0_0_0",
"symbol": "BTC",
"direction": "SHORT",
"payload": "{\"strategyCode\":\"HLSS\",\"period\":\"30m\",\"currency\":\"BTC\",\"isSale\":true,\"isClose\":false,\"price\":63150.38,\"lossPrice\":63623.3,\"gainPrices\":\"62677.470000000000000,62456.770000000000000\",\"openPrice2\":63535.02,\"totalGainTarget\":2,\"leverage\":100}",
"eventTime": 1786860019037
}`)
event, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
if event != "HLSS.sell" {
t.Fatalf("event=%q", event)
}
if got := asFloat(t, data["openPrice2"]); got != 63535.02 {
t.Fatalf("openPrice2=%v", data["openPrice2"])
}
if data["takeProfitRange"] != "62677.47-62456.77" {
t.Fatalf("takeProfitRange=%v", data["takeProfitRange"])
}
if data["entryRange"] != "63150.38-63535.02" {
t.Fatalf("entryRange=%v", data["entryRange"])
}
if _, ok := data["pushedAt"].(string); !ok {
t.Fatalf("pushedAt=%v", data["pushedAt"])
}
tmpl := `监控告警提醒
操作策略:高低点分型{{case .symbol "BTCUSDT" "BTC" "ETHUSDT" "ETH" "SOLUSDT" "SOL" "BNBUSDT" "BNB" .symbol}}-{{case .period "1h" "1小时" "4h" "4小时" "15m" "15分钟" "5m" "5分钟" "30m" "30分钟" "1d" "1日" .period}}周期{{case .side "LONG" "做多" "SHORT" "做空"}}
提醒时间:{{.pushedAt}}
{{with .takeProfitRange}}止盈目标:{{.}}
{{else}}{{with .takeProfitPrice}}止盈目标:{{.}}
{{end}}{{end}}{{with .entryRange}}介入区间:{{.}}
{{else}}{{with .price}}介入区间:{{.}}
{{end}}{{end}}{{with .stopLossPrice}}止损价位:{{.}}
{{end}}`
out, err := engine.NewRenderer().Render(tmpl, data)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"监控告警提醒",
"操作策略:高低点分型BTC-30分钟周期做空",
"止盈目标:62677.47-62456.77",
"介入区间:63150.38-63535.02",
"止损价位:63623.3",
} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in\n%s", want, out)
}
}
}
func asFloat(t *testing.T, v any) float64 {
t.Helper()
switch n := v.(type) {
case float64:
return n
case float32:
return float64(n)
default:
t.Fatalf("want float, got %T %v", v, v)
return 0
}
}
func TestConvertTinyPriceKeepsPrecision(t *testing.T) {
body := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"PEPE","direction":"LONG",
"payload":"{\"strategyCode\":\"ai-crypto-signals\",\"period\":\"1h\",\"currency\":\"PEPE\",\"price\":0.00000059,\"lossPrice\":0.00000055,\"gainPrices\":\"0.00000061,0.00000064\"}",
"eventTime":1786600000000
}`)
_, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
formatted, _ := data["formatted"].(string)
for _, want := range []string{
"开仓价格: 0.00000059\n",
"止损价格: 0.00000055\n",
} {
if !strings.Contains(formatted, want) {
t.Fatalf("missing %q in\n%s", want, formatted)
}
}
if strings.Contains(formatted, "开仓价格: 0.00\n") {
t.Fatalf("tiny price rounded to 0.00:\n%s", formatted)
}
out, err := engine.NewRenderer().Render("价格:{{.price}}\n止损:{{.stopLossPrice}}\n{{line \"止盈\" .takeProfitPrice}}", data)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{"价格:0.00000059", "止损:0.00000055"} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in rendered\n%s", want, out)
}
}
if strings.Contains(out, "e-") || strings.Contains(out, "E-") {
t.Fatalf("scientific notation in rendered\n%s", out)
}
}
func TestConvertInvalidJSON(t *testing.T) {
_, _, err := Convert([]byte(`{not json`))
if err == nil {
t.Fatal("expected error")
}
}
func TestConvertSaleIsOpenNotSell(t *testing.T) {
body := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"LTC","direction":"SHORT",
"payload":"{\"strategyCode\":\"ai-crypto-signals\",\"period\":\"1h\",\"currency\":\"LTC\",\"isSale\":true,\"isClose\":false,\"isGain\":false,\"gainTarget\":5,\"price\":44.68,\"lossPrice\":44.91,\"gainPrices\":\"44.565,44.45,44.335,44.22,44.105\",\"leverage\":58}",
"eventTime":1786894571102
}`)
event, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
if event != "trade.open" {
t.Fatalf("event=%q want trade.open", event)
}
formatted, _ := data["formatted"].(string)
if !strings.Contains(formatted, "空单开仓") {
t.Fatalf("formatted=%s", formatted)
}
if strings.Contains(formatted, "空单卖出") {
t.Fatalf("SHORT open must not say 卖出:\n%s", formatted)
}
}
func TestConvertTakeProfitIsGainEvent(t *testing.T) {
body := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"LTC","direction":"SHORT",
"payload":"{\"strategyCode\":\"ai-crypto-signals\",\"isSale\":true,\"isClose\":true,\"isGain\":true,\"gainTarget\":1,\"price\":44.63,\"remark\":\"{\\\"orderId\\\":\\\"x\\\",\\\"revenue\\\":\\\"14.2793%\\\",\\\"period\\\":\\\"53 min\\\"}\"}",
"eventTime":1786897763403
}`)
event, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
if event != "trade.gain" {
t.Fatalf("event=%q want trade.gain", event)
}
if data["holdPeriod"] != "53分钟" {
t.Fatalf("holdPeriod=%v", data["holdPeriod"])
}
if data["revenueDisplay"] != "+14.2793%" {
t.Fatalf("revenueDisplay=%v", data["revenueDisplay"])
}
if data["closeAction"] != "到达第一止盈 (TP1)" {
t.Fatalf("closeAction=%v", data["closeAction"])
}
}
func TestConvertStopLossIsCloseEvent(t *testing.T) {
body := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"ETH","direction":"SHORT",
"payload":"{\"strategyCode\":\"ai-crypto-signals\",\"isSale\":true,\"isClose\":true,\"isGain\":false,\"price\":1887,\"remark\":\"{\\\"orderId\\\":\\\"x\\\",\\\"revenue\\\":\\\"30.1557%%\\\",\\\"period\\\":\\\"signal\\\"}\"}",
"eventTime":1786897035113
}`)
event, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
if event != "trade.close" {
t.Fatalf("event=%q want trade.close", event)
}
if data["revenueDisplay"] != "-30.1557%" {
t.Fatalf("revenueDisplay=%v", data["revenueDisplay"])
}
if _, ok := data["holdPeriod"]; ok {
t.Fatalf("holdPeriod should be omitted for period=signal, got %v", data["holdPeriod"])
}
}
const aiCryptoOpenTmpl = `预警时间:{{.pushedAt}}
预警币种:{{.symbol}}
交易方向:{{case .side "LONG" "做多" "SHORT" "做空"}}
{{line "建议杠杆" .leverageText}}入场区域:{{.entryRange}}
{{line "风险控制(止损)" .stopLossPrice}}止盈目标:
{{with .tp1}}TP1{{.}}
{{end}}{{with .tp2}}TP2{{.}}
{{end}}{{with .tp3}}TP3{{.}}
{{end}}{{with .tp4}}TP4{{.}}
{{end}}{{with .tp5}}TP5{{.}}
{{end}}推送时间:{{.pushedAt}}`
const aiCryptoGainTmpl = `止盈时间:{{.pushedAt}}
预警币种:{{.symbol}}
执行操作:{{.closeAction}}
平仓点位:{{.price}}
预警收益:{{.revenueDisplay}}
{{line "预警周期" .holdPeriod}}`
const aiCryptoCloseTmpl = `止损时间:{{.pushedAt}}
预警币种:{{.symbol}}
执行操作:触发止损
平仓点位:{{.price}}
最终损益:{{.revenueDisplay}}
{{line "预警周期" .holdPeriod}}`
func TestRenderAICryptoOpenTemplate(t *testing.T) {
body := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"APE","direction":"LONG",
"payload":"{\"strategyCode\":\"ai-crypto-signals\",\"period\":\"1h\",\"currency\":\"APE\",\"isSale\":false,\"isClose\":false,\"isGain\":false,\"gainTarget\":5,\"price\":0.1235,\"lossPrice\":0.1223,\"gainPrices\":\"0.1241,0.1247,0.1253,0.1259,0.1265\",\"leverage\":31}",
"eventTime":1786850413251
}`)
_, data, err := Convert(body)
if err != nil {
t.Fatal(err)
}
out, err := engine.NewRenderer().Render(aiCryptoOpenTmpl, data)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"预警币种:APE",
"交易方向:做多",
"建议杠杆:31x",
"入场区域:0.1235",
"风险控制(止损):0.1223",
"TP10.1241",
"TP20.1247",
"TP30.1253",
"TP40.1259",
"TP50.1265",
} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in\n%s", want, out)
}
}
}
func TestRenderAICryptoGainAndCloseTemplates(t *testing.T) {
gainBody := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"LTC","direction":"SHORT",
"payload":"{\"strategyCode\":\"ai-crypto-signals\",\"isClose\":true,\"isGain\":true,\"gainTarget\":2,\"price\":0.1598,\"remark\":\"{\\\"revenue\\\":\\\"22.67%\\\",\\\"period\\\":\\\"1小时38分钟\\\"}\"}",
"eventTime":1786897763403
}`)
_, data, err := Convert(gainBody)
if err != nil {
t.Fatal(err)
}
out, err := engine.NewRenderer().Render(aiCryptoGainTmpl, data)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"预警币种:LTC",
"执行操作:到达第二止盈 (TP2)",
"平仓点位:0.1598",
"预警收益:+22.67%",
"预警周期:1小时38分钟",
} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in\n%s", want, out)
}
}
closeBody := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"ETH","direction":"SHORT",
"payload":"{\"strategyCode\":\"ai-crypto-signals\",\"isClose\":true,\"isGain\":false,\"price\":57.95,\"remark\":\"{\\\"revenue\\\":\\\"30.28%\\\",\\\"period\\\":\\\"2小时55分钟\\\"}\"}",
"eventTime":1786897035113
}`)
_, data, err = Convert(closeBody)
if err != nil {
t.Fatal(err)
}
out, err = engine.NewRenderer().Render(aiCryptoCloseTmpl, data)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"预警币种:ETH",
"执行操作:触发止损",
"平仓点位:57.95",
"最终损益:-30.28%",
"预警周期:2小时55分钟",
} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in\n%s", want, out)
}
}
}
const agTrendTmpl = `监控告警提醒
操作策略:AG趋势{{case .symbol "BTCUSDT" "BTC" "ETHUSDT" "ETH" "SOLUSDT" "SOL" "BNBUSDT" "BNB" .symbol}}-{{case .period "1h" "1小时" "2h" "2小时" "4h" "4小时" "6h" "6小时" "15m" "15分钟" "5m" "5分钟" "30m" "30分钟" "1d" "1日" .period}}周期{{case .side "LONG" "做多" "SHORT" "做空"}}
提醒时间:{{.pushedAt}}
{{with .takeProfitRange}}止盈目标:{{.}}
{{else}}{{with .takeProfitPrice}}止盈目标:{{.}}
{{end}}{{end}}{{with .entryRange}}介入区间:{{.}}
{{else}}{{with .price}}介入区间:{{.}}
{{end}}{{end}}{{with .stopLossPrice}}止损价位:{{.}}
{{end}}有效期:6天`
const anomalyAlertTmpl = `监控告警提醒
监控名称:异动预警
监控时间:{{.pushedAt}}
监控目标:{{case .symbol "BTCUSDT" "BTC" "ETHUSDT" "ETH" "SOLUSDT" "SOL" "BNBUSDT" "BNB" .symbol}}异动预警(暴涨/跌)生效
监控提醒:异动发生概率v1(v1<v2<v3)
有效期:2-4天`
const swingTrackTmpl = `监控告警提醒
监控名称:波段跟踪触发{{case .symbol "BTCUSDT" "BTC" "ETHUSDT" "ETH" "SOLUSDT" "SOL" "BNBUSDT" "BNB" .symbol}}-{{case .period "1h" "1小时" "2h" "2小时" "4h" "4小时" "6h" "6小时" "15m" "15分钟" "5m" "5分钟" "30m" "30分钟" "1d" "1日" .period}}周期{{case .side "LONG" "做多" "SHORT" "做空"}}
监控时间:{{.pushedAt}}
监控提醒:当前提醒价格{{with .takeProfitRange}}{{.}}{{else}}{{with .entryRange}}{{.}}{{else}}{{.price}}{{end}}{{end}}
监控状态:等待量化信号平仓
有效期: 17h`
func TestRenderAGAnomalySwingTemplates(t *testing.T) {
r := engine.NewRenderer()
ag, err := r.Render(agTrendTmpl, map[string]interface{}{
"symbol": "BTC",
"period": "6h",
"side": "LONG",
"pushedAt": "2026-08-07 16:00:20",
"takeProfitRange": "66821.8-67536.1",
"entryRange": "64938.6-64938.6",
"stopLossPrice": "62406.0",
})
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"操作策略:AG趋势BTC-6小时周期做多",
"提醒时间:2026-08-07 16:00:20",
"止盈目标:66821.8-67536.1",
"介入区间:64938.6-64938.6",
"止损价位:62406.0",
"有效期:6天",
} {
if !strings.Contains(ag, want) {
t.Fatalf("missing %q in\n%s", want, ag)
}
}
yd, err := r.Render(anomalyAlertTmpl, map[string]interface{}{
"symbol": "BTC",
"pushedAt": "2026-07-29 00:00:50",
})
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"监控名称:异动预警",
"监控时间:2026-07-29 00:00:50",
"监控目标:BTC异动预警(暴涨/跌)生效",
"监控提醒:异动发生概率v1(v1<v2<v3)",
"有效期:2-4天",
} {
if !strings.Contains(yd, want) {
t.Fatalf("missing %q in\n%s", want, yd)
}
}
bd, err := r.Render(swingTrackTmpl, map[string]interface{}{
"symbol": "ETH",
"period": "1h",
"side": "LONG",
"pushedAt": "2026-07-24 08:00:07",
"takeProfitRange": "1878.4-1894.2",
})
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"监控名称:波段跟踪触发ETH-1小时周期做多",
"监控时间:2026-07-24 08:00:07",
"监控提醒:当前提醒价格1878.4-1894.2",
"监控状态:等待量化信号平仓",
"有效期: 17h",
} {
if !strings.Contains(bd, want) {
t.Fatalf("missing %q in\n%s", want, bd)
}
}
}
func TestConvertAGTSAMAAndBTS(t *testing.T) {
agBody := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"BTC","direction":"SHORT",
"payload":"{\"strategyCode\":\"AGTS\",\"period\":\"6h\",\"currency\":\"BTC\",\"isSale\":true,\"isClose\":false,\"price\":63303,\"lossPrice\":65771.82,\"gainPrices\":\"61467.210000000000000,60770.880000000000000\",\"openPrice2\":63303.03,\"totalGainTarget\":2,\"leverage\":100}",
"eventTime":1786780820000
}`)
event, data, err := Convert(agBody)
if err != nil {
t.Fatal(err)
}
if event != "AGTS.open" {
t.Fatalf("AGTS event=%q", event)
}
if data["takeProfitRange"] != "61467.21-60770.88" {
t.Fatalf("takeProfitRange=%v", data["takeProfitRange"])
}
if data["entryRange"] != "63303-63303.03" {
t.Fatalf("entryRange=%v", data["entryRange"])
}
out, err := engine.NewRenderer().Render(agTrendTmpl, data)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"操作策略:AG趋势BTC-6小时周期做空",
"止盈目标:61467.21-60770.88",
"介入区间:63303-63303.03",
"止损价位:65771.82",
"有效期:6天",
} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in\n%s", want, out)
}
}
amaBody := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"BTC","direction":"LONG",
"payload":"{\"strategyCode\":\"AMA\",\"period\":\"4h\",\"currency\":\"BTC\",\"isSale\":false,\"isClose\":false,\"price\":63119.9,\"leverage\":100}",
"eventTime":1785312050000
}`)
event, data, err = Convert(amaBody)
if err != nil {
t.Fatal(err)
}
if event != "AMA.open" || data["strategyCode"] != "AMA" {
t.Fatalf("AMA event=%q data=%v", event, data)
}
out, err = engine.NewRenderer().Render(anomalyAlertTmpl, data)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "监控目标:BTC异动预警(暴涨/跌)生效") {
t.Fatalf("%s", out)
}
btsBody := []byte(`{
"eventType":"SIGNAL_RECEIVED","symbol":"BTC","direction":"LONG",
"payload":"{\"strategyCode\":\"BTS\",\"period\":\"2h\",\"currency\":\"BTC\",\"isSale\":false,\"isClose\":true,\"isGain\":false,\"price\":63533.2}",
"eventTime":1784865607000
}`)
event, data, err = Convert(btsBody)
if err != nil {
t.Fatal(err)
}
if event != "BTS.close" {
t.Fatalf("BTS event=%q want BTS.close", event)
}
out, err = engine.NewRenderer().Render(swingTrackTmpl, data)
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
"监控名称:波段跟踪触发BTC-2小时周期做多",
"监控提醒:当前提醒价格63533.2",
"监控状态:等待量化信号平仓",
"有效期: 17h",
} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in\n%s", want, out)
}
}
}