b3c42af53b
Motivation: HLSS 策略信号携带的止盈价位列表与介入价位含冗余小数位,直接推送可读性差,需转换为紧凑区间并补齐推送时间,提升通知的信息密度与可读性。 Changes: * 新增止盈目标区间,将止盈价位列表去零后连接为区间 * 新增介入区间,由开仓价与介入价组合生成 * 压缩价格数值多余小数位,保留原始精度 * 注入推送时间,便于模板渲染提醒时间
240 lines
7.0 KiB
Go
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")
|
|
}
|
|
}
|