6f5aaa9f85
Motivation: 此前的修复仅覆盖直接拼接文案的场景,跟单等基于模板渲染的推送场景中,%.2f 格式化仍会把 PEPE 等 Meme 币的极小价格(如 0.00000059)折叠为 0.00,导致交易通知丢失真实价格、误导用户。 Changes: * 抽取共享的价格显示逻辑到统一显示层,两个订阅者改为复用,移除重复实现 * 新增 Decimal 类型,渲染时以副本方式包装模板数据,使模板内 printf 风格格式化保留极小价格精度且不出现科学计数法,同时不修改调用方原始数据 * 渲染引擎空值判断改用反射实现,覆盖全部整型、无符号整型与浮点类型 * 补充转换到渲染的端到端回归测试,覆盖极小价格精度、科学计数法与数据不可变性
667 lines
21 KiB
Go
667 lines
21 KiB
Go
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",
|
||
"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<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)
|
||
}
|
||
}
|
||
}
|