cd3db8d453
Motivation: 部分 meme 币等标的价格极小(如 0.00000059),原有格式化统一保留两位小数会将其显示为 0.00,导致推送消息中的价格信息失真、误导用户;同时 viper 加载配置时会将嵌套 map 的 key 统一转为小写,导致按大写策略编码配置的策略覆盖项无法命中,仓位倍数、杠杆等覆盖参数失效。 Changes: * 新增价格展示格式化逻辑:绝对值不小于 1 的数值保留两位小数,小于 1 的数值采用最短精确表示,避免极小价格被截断为 0.00 * 策略覆盖查找改为大小写不敏感匹配,兼容 viper 将配置 key 小写化的行为,确保策略编码以任意大小写配置均可生效 * 补充极小价格展示、配置加载解析及策略覆盖匹配的单元测试
215 lines
5.7 KiB
Go
215 lines
5.7 KiB
Go
package tradesignal
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"aiaa-notification-service/internal/tz"
|
|
)
|
|
|
|
type FormatOptions struct {
|
|
AvgPrice *float64
|
|
}
|
|
|
|
func Format(signal *Signal, opts ...FormatOptions) string {
|
|
var opt FormatOptions
|
|
if len(opts) > 0 {
|
|
opt = opts[0]
|
|
}
|
|
|
|
title := actionTitle(signal.Side, signal.Action)
|
|
symbol := trimQuote(signal.Symbol)
|
|
|
|
lines := []string{title}
|
|
lines = append(lines, fmt.Sprintf("交易品种: %s", symbol))
|
|
if p := strings.TrimSpace(signal.Period); p != "" {
|
|
lines = append(lines, fmt.Sprintf("周期: %s", p))
|
|
}
|
|
|
|
action := strings.ToUpper(signal.Action)
|
|
switch action {
|
|
case "OPEN":
|
|
lines = append(lines, fmt.Sprintf("开仓价格: %s", formatPrice(signal.Price)))
|
|
if line := sizeLine("OPEN", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
|
lines = append(lines, line)
|
|
}
|
|
lines = appendAvgPrice(lines, opt.AvgPrice)
|
|
if signal.Leverage > 0 {
|
|
lines = append(lines, fmt.Sprintf("杠杆: %dx", signal.Leverage))
|
|
}
|
|
if signal.TakeProfitPrice != nil {
|
|
lines = append(lines, fmt.Sprintf("止盈价格: %s", formatPrice(*signal.TakeProfitPrice)))
|
|
}
|
|
if signal.StopLossPrice != nil {
|
|
lines = append(lines, fmt.Sprintf("止损价格: %s", formatPrice(*signal.StopLossPrice)))
|
|
}
|
|
case "CLOSE":
|
|
lines = append(lines, fmt.Sprintf("平仓价格: %s", formatPrice(signal.Price)))
|
|
lines = append(lines, closeSizeLine(signal.Quantity, signal.PosMarginRatio))
|
|
lines = appendAvgPrice(lines, opt.AvgPrice)
|
|
if signal.PnL != nil {
|
|
lines = append(lines, fmt.Sprintf("平仓盈亏: %s", formatPrice(*signal.PnL)))
|
|
}
|
|
if signal.AccountBalance != nil {
|
|
lines = append(lines, fmt.Sprintf("账户余额:%s", formatPrice(*signal.AccountBalance)))
|
|
}
|
|
case "ADD":
|
|
lines = append(lines, fmt.Sprintf("加仓价格: %s", formatPrice(signal.Price)))
|
|
if line := sizeLine("ADD", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
|
lines = append(lines, line)
|
|
}
|
|
lines = appendAvgPrice(lines, opt.AvgPrice)
|
|
if signal.Leverage > 0 {
|
|
lines = append(lines, fmt.Sprintf("杠杆: %dx", signal.Leverage))
|
|
}
|
|
case "REDUCE":
|
|
lines = append(lines, fmt.Sprintf("减仓价格: %s", formatPrice(signal.Price)))
|
|
if line := sizeLine("REDUCE", signal.Quantity, signal.PosMarginRatio); line != "" {
|
|
lines = append(lines, line)
|
|
}
|
|
lines = appendAvgPrice(lines, opt.AvgPrice)
|
|
if signal.PnL != nil {
|
|
lines = append(lines, fmt.Sprintf("减仓盈亏: %s", formatPrice(*signal.PnL)))
|
|
}
|
|
if signal.AccountBalance != nil {
|
|
lines = append(lines, fmt.Sprintf("账户余额:%s", formatPrice(*signal.AccountBalance)))
|
|
}
|
|
default:
|
|
lines = append(lines, fmt.Sprintf("价格: %s", formatPrice(signal.Price)))
|
|
if line := sizeLine("", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
|
lines = append(lines, line)
|
|
}
|
|
lines = appendAvgPrice(lines, opt.AvgPrice)
|
|
}
|
|
|
|
if signal.StrategyCode != "" {
|
|
lines = append(lines, fmt.Sprintf("策略: %s", signal.StrategyCode))
|
|
}
|
|
|
|
eventTime := signal.ParsedEventTime().In(tz.CST)
|
|
lines = append(lines, fmt.Sprintf("Time: %s", eventTime.Format("2006.01.02 15:04:05")))
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func actionTitle(side, action string) string {
|
|
side = strings.ToUpper(side)
|
|
action = strings.ToUpper(action)
|
|
|
|
var pos string
|
|
switch side {
|
|
case "LONG":
|
|
pos = "多单"
|
|
case "SHORT":
|
|
pos = "空单"
|
|
default:
|
|
pos = side
|
|
}
|
|
|
|
var act string
|
|
switch action {
|
|
case "OPEN":
|
|
act = "开仓"
|
|
case "ADD":
|
|
act = "加仓"
|
|
case "CLOSE":
|
|
act = "平仓"
|
|
case "REDUCE":
|
|
act = "减仓"
|
|
default:
|
|
act = action
|
|
}
|
|
|
|
return pos + act
|
|
}
|
|
|
|
func appendAvgPrice(lines []string, avgPrice *float64) []string {
|
|
if avgPrice == nil || *avgPrice <= 0 {
|
|
return lines
|
|
}
|
|
return append(lines, fmt.Sprintf("平均单价: %s", formatPrice(*avgPrice)))
|
|
}
|
|
|
|
func closeSizeLine(quantity, posMarginRatio *float64) string {
|
|
if quantity != nil && *quantity > 0 {
|
|
return fmt.Sprintf("平仓数量: %.2f", *quantity)
|
|
}
|
|
ratio := 1.0
|
|
if posMarginRatio != nil {
|
|
ratio = *posMarginRatio
|
|
}
|
|
return fmt.Sprintf("平仓比例: %s", formatPercent(ratio))
|
|
}
|
|
|
|
func sizeLine(action string, quantity, marginRatio *float64) string {
|
|
if quantity != nil && *quantity > 0 {
|
|
return fmt.Sprintf("%s: %.2f", quantityLabel(action), *quantity)
|
|
}
|
|
if marginRatio != nil {
|
|
return fmt.Sprintf("%s: %s", ratioLabel(action), formatPercent(*marginRatio))
|
|
}
|
|
if quantity != nil {
|
|
return fmt.Sprintf("%s: %.2f", quantityLabel(action), *quantity)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func quantityLabel(action string) string {
|
|
switch strings.ToUpper(action) {
|
|
case "OPEN":
|
|
return "开仓数量"
|
|
case "ADD":
|
|
return "加仓数量"
|
|
case "CLOSE":
|
|
return "平仓数量"
|
|
case "REDUCE":
|
|
return "减仓数量"
|
|
default:
|
|
return "数量"
|
|
}
|
|
}
|
|
|
|
func ratioLabel(action string) string {
|
|
switch strings.ToUpper(action) {
|
|
case "OPEN":
|
|
return "开仓比例"
|
|
case "ADD":
|
|
return "加仓比例"
|
|
case "CLOSE":
|
|
return "平仓比例"
|
|
case "REDUCE":
|
|
return "减仓比例"
|
|
default:
|
|
return "仓位比例"
|
|
}
|
|
}
|
|
|
|
func formatPercent(ratio float64) string {
|
|
return fmt.Sprintf("%.2f%%", ratio*100)
|
|
}
|
|
|
|
// 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 trimQuote(symbol string) string {
|
|
symbol = strings.ToUpper(symbol)
|
|
for _, suffix := range []string{"USDT", "USDC", "BUSD", "USD"} {
|
|
if strings.HasSuffix(symbol, suffix) && len(symbol) > len(suffix) {
|
|
return symbol[:len(symbol)-len(suffix)]
|
|
}
|
|
}
|
|
return symbol
|
|
}
|