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:
2026-08-17 00:54:01 +08:00
parent 0f9f154969
commit f5f64f7653
4 changed files with 412 additions and 6 deletions
+96 -5
View File
@@ -38,6 +38,7 @@ type payload struct {
type remark struct {
OrderID string `json:"orderId"`
Revenue string `json:"revenue"`
Period string `json:"period"`
}
type Converter struct{}
@@ -99,12 +100,28 @@ func Convert(body []byte) (string, map[string]interface{}, error) {
if p.TotalGainTarget != 0 {
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 != "" {
data["orderId"] = r.OrderID
}
if 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
@@ -153,13 +170,23 @@ func mergePayloadFields(data map[string]interface{}, payloadJSON []byte) {
}
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 {
case p.IsClose:
return "CLOSE"
case p.IsGain:
return "GAIN"
case p.IsSale:
return "SELL"
case p.IsClose:
return "CLOSE"
default:
return "OPEN"
}
@@ -320,3 +347,67 @@ func firstNonEmpty(a, b string) string {
}
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)
}
}