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) } } 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", "TP1:0.1241", "TP2:0.1247", "TP3:0.1253", "TP4:0.1259", "TP5:0.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