Files
aiaa-notification-server/internal/subscriber/cryptostrategy/convert_test.go
T
ryan b3c42af53b feat(订阅): 支持 HLSS 策略止盈与介入区间格式化
Motivation:
HLSS 策略信号携带的止盈价位列表与介入价位含冗余小数位,直接推送可读性差,需转换为紧凑区间并补齐推送时间,提升通知的信息密度与可读性。

Changes:

* 新增止盈目标区间,将止盈价位列表去零后连接为区间
* 新增介入区间,由开仓价与介入价组合生成
* 压缩价格数值多余小数位,保留原始精度
* 注入推送时间,便于模板渲染提醒时间
2026-08-16 23:21:59 +08:00

240 lines
7.0 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 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, "CLOSE") {
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 TestConvertInvalidJSON(t *testing.T) {
_, _, err := Convert([]byte(`{not json`))
if err == nil {
t.Fatal("expected error")
}
}