package cryptostrategy import ( "bytes" "encoding/json" "fmt" "math" "strconv" "strings" "time" "aiaa-notification-service/internal/tz" ) type envelope struct { EventType string `json:"eventType"` CorrelationID string `json:"correlationId"` Symbol string `json:"symbol"` Direction string `json:"direction"` Payload json.RawMessage `json:"payload"` EventTime int64 `json:"eventTime"` } type payload struct { StrategyCode string `json:"strategyCode"` Period string `json:"period"` Currency string `json:"currency"` IsSale bool `json:"isSale"` IsClose bool `json:"isClose"` IsGain bool `json:"isGain"` GainTarget float64 `json:"gainTarget"` Price float64 `json:"price"` LossPrice float64 `json:"lossPrice"` GainPrices string `json:"gainPrices"` OpenPrice2 float64 `json:"openPrice2"` Remark string `json:"remark"` TotalGainTarget float64 `json:"totalGainTarget"` Leverage int `json:"leverage"` } type remark struct { OrderID string `json:"orderId"` Revenue string `json:"revenue"` Period string `json:"period"` } type Converter struct{} func NewConverter() *Converter { return &Converter{} } func (c *Converter) Convert(body []byte) (string, map[string]interface{}, error) { return Convert(body) } func Convert(body []byte) (string, map[string]interface{}, error) { var env envelope if err := json.Unmarshal(body, &env); err != nil { return "", nil, fmt.Errorf("invalid envelope: %w", err) } p, payloadJSON, err := parsePayload(body, env.Payload) if err != nil { return "", nil, err } action := inferAction(p) event := eventName(p.StrategyCode, action) text := format(env, p, action) side := strings.ToUpper(strings.TrimSpace(env.Direction)) if side == "" { if p.IsSale { side = "SHORT" } else { side = "LONG" } } data := map[string]interface{}{ "eventType": env.EventType, "correlationId": env.CorrelationID, "symbol": firstNonEmpty(env.Symbol, p.Currency), "direction": firstNonEmpty(env.Direction, side), "side": side, "action": action, "eventTime": env.EventTime, "strategyCode": p.StrategyCode, "period": p.Period, "currency": p.Currency, "isSale": p.IsSale, "isClose": p.IsClose, "isGain": p.IsGain, "gainTarget": p.GainTarget, "price": p.Price, "lossPrice": p.LossPrice, "gainPrices": p.GainPrices, "openPrice2": p.OpenPrice2, "leverage": p.Leverage, "formatted": text, "stopLossPrice": p.LossPrice, "takeProfitPrice": takeProfitPrice(p), "takeProfitRange": formatPriceRange(p.GainPrices), "entryRange": entryRange(p.Price, p.OpenPrice2), "totalAvgPx": "", } mergePayloadFields(data, payloadJSON) if env.EventTime > 0 { data["pushedAt"] = tz.Format(time.UnixMilli(env.EventTime), "2006-01-02 15:04:05") } if p.TotalGainTarget != 0 { data["totalGainTarget"] = p.TotalGainTarget } 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 } func parsePayload(body []byte, raw json.RawMessage) (payload, []byte, error) { var p payload raw = bytes.TrimSpace(raw) if len(raw) == 0 || string(raw) == "null" { if err := json.Unmarshal(body, &p); err != nil { return p, nil, fmt.Errorf("invalid payload: %w", err) } return p, body, nil } var asString string if err := json.Unmarshal(raw, &asString); err == nil { asString = strings.TrimSpace(asString) if asString == "" { if err := json.Unmarshal(body, &p); err != nil { return p, nil, fmt.Errorf("invalid payload: %w", err) } return p, body, nil } raw = []byte(asString) } if err := json.Unmarshal(raw, &p); err != nil { return p, nil, fmt.Errorf("invalid payload: %w", err) } return p, raw, nil } func mergePayloadFields(data map[string]interface{}, payloadJSON []byte) { if len(bytes.TrimSpace(payloadJSON)) == 0 { return } var extra map[string]interface{} if err := json.Unmarshal(payloadJSON, &extra); err != nil { return } for k, v := range extra { if _, ok := data[k]; ok { continue } data[k] = v } } func eventName(strategyCode, action string) string { code := strings.ToUpper(strings.TrimSpace(strategyCode)) suffix := strings.ToLower(action) switch code { case "HLSS", "AMA", "BTS", "AGTS": return code + "." + suffix default: return "trade." + suffix } } 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.IsGain: return "GAIN" case p.IsClose: return "CLOSE" default: return "OPEN" } } func takeProfitPrice(p payload) interface{} { if gp := strings.TrimSpace(p.GainPrices); gp != "" { return gp } if p.IsGain && p.Price > 0 { return p.Price } return "" } func parseRemark(raw string) remark { raw = strings.TrimSpace(raw) if raw == "" { return remark{} } var r remark if err := json.Unmarshal([]byte(raw), &r); err != nil { return remark{} } return r } func format(env envelope, p payload, action string) string { symbol := firstNonEmpty(env.Symbol, p.Currency) lines := []string{actionTitle(env.Direction, action)} if symbol != "" { lines = append(lines, fmt.Sprintf("交易品种: %s", symbol)) } if p.Period != "" { lines = append(lines, fmt.Sprintf("周期: %s", p.Period)) } switch action { case "CLOSE": if p.Price > 0 { lines = append(lines, fmt.Sprintf("平仓价格: %s", formatPrice(p.Price))) } case "GAIN": if p.Price > 0 { lines = append(lines, fmt.Sprintf("止盈价格: %s", formatPrice(p.Price))) } case "SELL": if p.Price > 0 { lines = append(lines, fmt.Sprintf("卖出价格: %s", formatPrice(p.Price))) } default: if p.Price > 0 { lines = append(lines, fmt.Sprintf("开仓价格: %s", formatPrice(p.Price))) } } if p.LossPrice > 0 { lines = append(lines, fmt.Sprintf("止损价格: %s", formatPrice(p.LossPrice))) } if er := entryRange(p.Price, p.OpenPrice2); er != "" && p.OpenPrice2 != 0 { lines = append(lines, fmt.Sprintf("介入区间: %s", er)) } if gp := strings.TrimSpace(p.GainPrices); gp != "" && action != "GAIN" { lines = append(lines, fmt.Sprintf("止盈价格: %s", strings.Join(splitPrices(gp), ", "))) } if p.GainTarget != 0 { lines = append(lines, fmt.Sprintf("止盈目标: %g", p.GainTarget)) } if p.Leverage > 0 { lines = append(lines, fmt.Sprintf("杠杆: %dx", p.Leverage)) } if p.StrategyCode != "" { lines = append(lines, fmt.Sprintf("策略: %s", p.StrategyCode)) } if env.EventTime > 0 { t := time.UnixMilli(env.EventTime).In(tz.CST) lines = append(lines, fmt.Sprintf("Time: %s", t.Format("2006.01.02 15:04:05"))) } return strings.Join(lines, "\n") } func actionTitle(direction, action string) string { var pos string switch strings.ToUpper(direction) { case "LONG": pos = "多单" case "SHORT": pos = "空单" default: pos = direction } var act string switch action { case "OPEN": act = "开仓" case "CLOSE": act = "平仓" case "GAIN": act = "止盈" case "SELL": act = "卖出" default: act = action } return pos + act } func splitPrices(s string) []string { parts := strings.Split(s, ",") out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { out = append(out, p) } } return out } func formatPriceRange(s string) string { parts := splitPrices(s) if len(parts) == 0 { return "" } out := make([]string, 0, len(parts)) for _, p := range parts { out = append(out, compactPrice(p)) } return strings.Join(out, "-") } func entryRange(price, open2 float64) string { switch { case price != 0 && open2 != 0: return formatFloat(price) + "-" + formatFloat(open2) case price != 0: return formatFloat(price) case open2 != 0: return formatFloat(open2) default: return "" } } func compactPrice(s string) string { f, err := strconv.ParseFloat(strings.TrimSpace(s), 64) if err != nil { return strings.TrimSpace(s) } return formatFloat(f) } func formatFloat(f float64) string { return strconv.FormatFloat(f, 'f', -1, 64) } // formatPrice renders a price for display. Values >= 1 keep two decimals for // readability; smaller values use the shortest exact representation so tiny // prices like 0.00000059 are not collapsed to 0.00. func formatPrice(v float64) string { if v == 0 { return "0.00" } if math.Abs(v) >= 1 { return strconv.FormatFloat(v, 'f', 2, 64) } return strconv.FormatFloat(v, 'f', -1, 64) } func firstNonEmpty(a, b string) string { if strings.TrimSpace(a) != "" { return a } 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) } }