feat(crypto策略): 支持 AI 信号止盈止损事件与多字段渲染
Motivation: 区分止盈与止损事件,并为 AI crypto signals 策略提供更丰富的推送内容渲染能力(杠杆、止盈档位、收益、持仓周期等)。 Changes: * 开仓(多/空,含原 isSale 空单)统一映射为 trade.open,不再发 trade.sell * 新增 trade.gain 事件用于止盈,止损仍映射为 trade.close;HLSS 策略保持原有事件映射 * 新增 leverageText、tp1~tp5、closeAction、revenueDisplay、holdPeriod 等渲染字段 * 新增收益符号、止盈档位、持仓周期等格式化逻辑 * 补充 AI crypto signals 的模板与规则文档及单元测试
This commit is contained in:
@@ -152,7 +152,7 @@ func TestConvertRendersSharedSignalTemplate(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "ICP") || !strings.Contains(out, "CLOSE") {
|
||||
if !strings.Contains(out, "ICP") || !strings.Contains(out, "GAIN") {
|
||||
t.Fatalf("out=%s", out)
|
||||
}
|
||||
}
|
||||
@@ -237,3 +237,180 @@ func TestConvertInvalidJSON(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user