ae25409e56
Motivation: 统一 crypto-strategy 与 trade-signal 两类交易信号的模板字段,使同一套通知模板可复用;字段缺失或为空时不再导致渲染报错或输出空行,通知内容更整洁。 Changes: * 渲染器缺失字段改为按空值处理,新增 line 函数实现空值整行省略 * crypto-strategy 信号补充止盈价、止损价、平均价、收益额等模板字段 * trade-signal 信号保留原始报文中的额外字段以适配统一模板 * 修复结构化日志将 error 作为值直接输出导致的格式问题
166 lines
4.8 KiB
Go
166 lines
4.8 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 TestConvertInvalidJSON(t *testing.T) {
|
|
_, _, err := Convert([]byte(`{not json`))
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
}
|