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:
@@ -102,6 +102,8 @@ make build && ./bin/server
|
|||||||
|
|
||||||
环境变量 `RABBITMQ_URL` 未设置时不启动消费,HTTP 通知不受影响。交易信号订阅需事先创建 Source(如 `trade-signal`)、模板(可用 `{{.formatted}}`)、规则 `trade.open` / `trade.add` / `trade.close` / `trade.reduce`、以及渠道。规则条件可用 `strategyCode` / `symbol` / `period`。
|
环境变量 `RABBITMQ_URL` 未设置时不启动消费,HTTP 通知不受影响。交易信号订阅需事先创建 Source(如 `trade-signal`)、模板(可用 `{{.formatted}}`)、规则 `trade.open` / `trade.add` / `trade.close` / `trade.reduce`、以及渠道。规则条件可用 `strategyCode` / `symbol` / `period`。
|
||||||
|
|
||||||
|
`crypto-strategy` 开仓(多/空,含原来的 `isSale` 空单)都映射为 `trade.open`,不再发 `trade.sell`。止盈(`isGain`)为 `trade.gain`,止损(`isClose` 且非 `isGain`)为 `trade.close`。高低分 `HLSS` 仍用 `HLSS.open` / `HLSS.sell` / `HLSS.close`。同一 Source 每个 event 只能有一条规则,所以止盈不能再和止损共用 `trade.close`。AI crypto signals 的现成模板与规则见 `docs/httpie/curls.md`。
|
||||||
|
|
||||||
健康检查:`GET /health` → `{"status":"ok"}`
|
健康检查:`GET /health` → `{"status":"ok"}`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -429,3 +429,139 @@ curl -X PATCH 'http://localhost:8080/api/v1/rules/1/channels/1/disable' \
|
|||||||
curl -X GET 'http://localhost:8080/api/v1/message-logs?source=trading-system&event=trade.open&status=success&page=1&page_size=20' \
|
curl -X GET 'http://localhost:8080/api/v1/message-logs?source=trading-system&event=trade.open&status=success&page=1&page_size=20' \
|
||||||
-H 'Authorization: Bearer admin-sk-change-me'
|
-H 'Authorization: Bearer admin-sk-change-me'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI crypto signals(crypto-strategy)
|
||||||
|
|
||||||
|
Source:`crypto-strategy`。渠道:`safew_ai_crypto_signals`。条件:`strategyCode = ai-crypto-signals`。
|
||||||
|
|
||||||
|
事件:开仓(多/空)→ `trade.open`;止盈 → `trade.gain`;止损 → `trade.close`。空单开仓不再发 `trade.sell`。
|
||||||
|
|
||||||
|
开仓渲染示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
预警时间:2026-08-16 03:20:13
|
||||||
|
预警币种:APE
|
||||||
|
交易方向:做多
|
||||||
|
建议杠杆:31x
|
||||||
|
入场区域:0.1235
|
||||||
|
风险控制(止损):0.1223
|
||||||
|
止盈目标:
|
||||||
|
TP1:0.1241
|
||||||
|
TP2:0.1247
|
||||||
|
TP3:0.1253
|
||||||
|
TP4:0.1259
|
||||||
|
TP5:0.1265
|
||||||
|
推送时间:2026-08-16 03:20:13
|
||||||
|
```
|
||||||
|
|
||||||
|
止盈渲染示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
止盈时间:2026-08-16 16:29:23
|
||||||
|
预警币种:LTC
|
||||||
|
执行操作:到达第一止盈 (TP1)
|
||||||
|
平仓点位:44.63
|
||||||
|
预警收益:+14.2793%
|
||||||
|
预警周期:53分钟
|
||||||
|
```
|
||||||
|
|
||||||
|
止损渲染示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
止损时间:2026-08-16 16:17:15
|
||||||
|
预警币种:ETH
|
||||||
|
执行操作:触发止损
|
||||||
|
平仓点位:1887
|
||||||
|
最终损益:-30.1557%
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建开仓模板
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/templates' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "ai_crypto_signals_open",
|
||||||
|
"content": "预警时间:{{.pushedAt}}\n预警币种:{{.symbol}}\n交易方向:{{case .side \"LONG\" \"做多\" \"SHORT\" \"做空\"}}\n{{line \"建议杠杆\" .leverageText}}入场区域:{{.entryRange}}\n{{line \"风险控制(止损)\" .stopLossPrice}}止盈目标:\n{{with .tp1}}TP1:{{.}}\n{{end}}{{with .tp2}}TP2:{{.}}\n{{end}}{{with .tp3}}TP3:{{.}}\n{{end}}{{with .tp4}}TP4:{{.}}\n{{end}}{{with .tp5}}TP5:{{.}}\n{{end}}推送时间:{{.pushedAt}}"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建止盈模板
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/templates' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "ai_crypto_signals_gain",
|
||||||
|
"content": "止盈时间:{{.pushedAt}}\n预警币种:{{.symbol}}\n执行操作:{{.closeAction}}\n平仓点位:{{.price}}\n预警收益:{{.revenueDisplay}}\n{{line \"预警周期\" .holdPeriod}}"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建止损模板
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/templates' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "ai_crypto_signals_close",
|
||||||
|
"content": "止损时间:{{.pushedAt}}\n预警币种:{{.symbol}}\n执行操作:触发止损\n平仓点位:{{.price}}\n最终损益:{{.revenueDisplay}}\n{{line \"预警周期\" .holdPeriod}}"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建开仓 / 止盈 / 止损规则
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/rules' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "AI crypto signals 开仓",
|
||||||
|
"source_name": "crypto-strategy",
|
||||||
|
"event": "trade.open",
|
||||||
|
"template_name": "ai_crypto_signals_open",
|
||||||
|
"channels": ["safew_ai_crypto_signals"],
|
||||||
|
"conditions": [
|
||||||
|
{"field": "strategyCode", "op": "eq", "value": "ai-crypto-signals"}
|
||||||
|
],
|
||||||
|
"enabled": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/rules' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "AI crypto signals 止盈",
|
||||||
|
"source_name": "crypto-strategy",
|
||||||
|
"event": "trade.gain",
|
||||||
|
"template_name": "ai_crypto_signals_gain",
|
||||||
|
"channels": ["safew_ai_crypto_signals"],
|
||||||
|
"conditions": [
|
||||||
|
{"field": "strategyCode", "op": "eq", "value": "ai-crypto-signals"}
|
||||||
|
],
|
||||||
|
"enabled": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST 'http://localhost:8080/api/v1/rules' \
|
||||||
|
-H 'Authorization: Bearer admin-sk-change-me' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"name": "AI crypto signals 止损",
|
||||||
|
"source_name": "crypto-strategy",
|
||||||
|
"event": "trade.close",
|
||||||
|
"template_name": "ai_crypto_signals_close",
|
||||||
|
"channels": ["safew_ai_crypto_signals"],
|
||||||
|
"conditions": [
|
||||||
|
{"field": "strategyCode", "op": "eq", "value": "ai-crypto-signals"}
|
||||||
|
],
|
||||||
|
"enabled": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type payload struct {
|
|||||||
type remark struct {
|
type remark struct {
|
||||||
OrderID string `json:"orderId"`
|
OrderID string `json:"orderId"`
|
||||||
Revenue string `json:"revenue"`
|
Revenue string `json:"revenue"`
|
||||||
|
Period string `json:"period"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Converter struct{}
|
type Converter struct{}
|
||||||
@@ -99,12 +100,28 @@ func Convert(body []byte) (string, map[string]interface{}, error) {
|
|||||||
if p.TotalGainTarget != 0 {
|
if p.TotalGainTarget != 0 {
|
||||||
data["totalGainTarget"] = p.TotalGainTarget
|
data["totalGainTarget"] = p.TotalGainTarget
|
||||||
}
|
}
|
||||||
if r := parseRemark(p.Remark); r.OrderID != "" || r.Revenue != "" {
|
if p.Leverage > 0 {
|
||||||
|
data["leverageText"] = fmt.Sprintf("%dx", p.Leverage)
|
||||||
|
}
|
||||||
|
for i, price := range splitPrices(p.GainPrices) {
|
||||||
|
if i >= 5 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
data[fmt.Sprintf("tp%d", i+1)] = compactPrice(price)
|
||||||
|
}
|
||||||
|
if p.IsGain {
|
||||||
|
data["closeAction"] = formatTPAction(p.GainTarget)
|
||||||
|
}
|
||||||
|
if r := parseRemark(p.Remark); r.OrderID != "" || r.Revenue != "" || r.Period != "" {
|
||||||
if r.OrderID != "" {
|
if r.OrderID != "" {
|
||||||
data["orderId"] = r.OrderID
|
data["orderId"] = r.OrderID
|
||||||
}
|
}
|
||||||
if r.Revenue != "" {
|
if r.Revenue != "" {
|
||||||
data["revenue"] = r.Revenue
|
data["revenue"] = r.Revenue
|
||||||
|
data["revenueDisplay"] = formatRevenue(r.Revenue, p.IsGain)
|
||||||
|
}
|
||||||
|
if hp := formatHoldPeriod(r.Period); hp != "" {
|
||||||
|
data["holdPeriod"] = hp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return event, data, nil
|
return event, data, nil
|
||||||
@@ -153,13 +170,23 @@ func mergePayloadFields(data map[string]interface{}, payloadJSON []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func inferAction(p payload) string {
|
func inferAction(p payload) string {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(p.StrategyCode), "HLSS") {
|
||||||
|
switch {
|
||||||
|
case p.IsClose:
|
||||||
|
return "CLOSE"
|
||||||
|
case p.IsGain:
|
||||||
|
return "GAIN"
|
||||||
|
case p.IsSale:
|
||||||
|
return "SELL"
|
||||||
|
default:
|
||||||
|
return "OPEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case p.IsClose:
|
|
||||||
return "CLOSE"
|
|
||||||
case p.IsGain:
|
case p.IsGain:
|
||||||
return "GAIN"
|
return "GAIN"
|
||||||
case p.IsSale:
|
case p.IsClose:
|
||||||
return "SELL"
|
return "CLOSE"
|
||||||
default:
|
default:
|
||||||
return "OPEN"
|
return "OPEN"
|
||||||
}
|
}
|
||||||
@@ -320,3 +347,67 @@ func firstNonEmpty(a, b string) string {
|
|||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatTPAction(gainTarget float64) string {
|
||||||
|
n := int(gainTarget)
|
||||||
|
names := []string{"", "第一", "第二", "第三", "第四", "第五"}
|
||||||
|
if n >= 1 && n < len(names) {
|
||||||
|
return fmt.Sprintf("到达%s止盈 (TP%d)", names[n], n)
|
||||||
|
}
|
||||||
|
return "到达止盈"
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatRevenue(raw string, isGain bool) string {
|
||||||
|
s := strings.ReplaceAll(strings.TrimSpace(raw), "%", "")
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(s, "+"), strings.HasPrefix(s, "-"):
|
||||||
|
return s + "%"
|
||||||
|
case isGain:
|
||||||
|
return "+" + s + "%"
|
||||||
|
default:
|
||||||
|
return "-" + s + "%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatHoldPeriod(raw string) string {
|
||||||
|
s := strings.TrimSpace(raw)
|
||||||
|
if s == "" || strings.EqualFold(s, "signal") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if strings.Contains(s, "小时") || strings.Contains(s, "分钟") {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(s)
|
||||||
|
if n, ok := parseTrailingNumber(strings.TrimSpace(strings.TrimSuffix(lower, "min"))); ok {
|
||||||
|
return formatMinutes(n)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTrailingNumber(s string) (int, bool) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
n, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMinutes(n int) string {
|
||||||
|
if n <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
h, m := n/60, n%60
|
||||||
|
switch {
|
||||||
|
case h > 0 && m > 0:
|
||||||
|
return fmt.Sprintf("%d小时%d分钟", h, m)
|
||||||
|
case h > 0:
|
||||||
|
return fmt.Sprintf("%d小时", h)
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d分钟", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ func TestConvertRendersSharedSignalTemplate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("render: %v", err)
|
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)
|
t.Fatalf("out=%s", out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,3 +237,180 @@ func TestConvertInvalidJSON(t *testing.T) {
|
|||||||
t.Fatal("expected error")
|
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