fix(交易信号): 修复极小价格展示精度丢失与策略覆盖匹配失效
Motivation: 部分 meme 币等标的价格极小(如 0.00000059),原有格式化统一保留两位小数会将其显示为 0.00,导致推送消息中的价格信息失真、误导用户;同时 viper 加载配置时会将嵌套 map 的 key 统一转为小写,导致按大写策略编码配置的策略覆盖项无法命中,仓位倍数、杠杆等覆盖参数失效。 Changes: * 新增价格展示格式化逻辑:绝对值不小于 1 的数值保留两位小数,小于 1 的数值采用最短精确表示,避免极小价格被截断为 0.00 * 策略覆盖查找改为大小写不敏感匹配,兼容 viper 将配置 key 小写化的行为,确保策略编码以任意大小写配置均可生效 * 补充极小价格展示、配置加载解析及策略覆盖匹配的单元测试
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -244,23 +245,23 @@ func format(env envelope, p payload, action string) string {
|
||||
switch action {
|
||||
case "CLOSE":
|
||||
if p.Price > 0 {
|
||||
lines = append(lines, fmt.Sprintf("平仓价格: %.2f", p.Price))
|
||||
lines = append(lines, fmt.Sprintf("平仓价格: %s", formatPrice(p.Price)))
|
||||
}
|
||||
case "GAIN":
|
||||
if p.Price > 0 {
|
||||
lines = append(lines, fmt.Sprintf("止盈价格: %.2f", p.Price))
|
||||
lines = append(lines, fmt.Sprintf("止盈价格: %s", formatPrice(p.Price)))
|
||||
}
|
||||
case "SELL":
|
||||
if p.Price > 0 {
|
||||
lines = append(lines, fmt.Sprintf("卖出价格: %.2f", p.Price))
|
||||
lines = append(lines, fmt.Sprintf("卖出价格: %s", formatPrice(p.Price)))
|
||||
}
|
||||
default:
|
||||
if p.Price > 0 {
|
||||
lines = append(lines, fmt.Sprintf("开仓价格: %.2f", p.Price))
|
||||
lines = append(lines, fmt.Sprintf("开仓价格: %s", formatPrice(p.Price)))
|
||||
}
|
||||
}
|
||||
if p.LossPrice > 0 {
|
||||
lines = append(lines, fmt.Sprintf("止损价格: %.2f", p.LossPrice))
|
||||
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))
|
||||
@@ -359,6 +360,19 @@ 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
|
||||
|
||||
@@ -250,6 +250,30 @@ func asFloat(t *testing.T, v any) float64 {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertTinyPriceKeepsPrecision(t *testing.T) {
|
||||
body := []byte(`{
|
||||
"eventType":"SIGNAL_RECEIVED","symbol":"PEPE","direction":"LONG",
|
||||
"payload":"{\"strategyCode\":\"ai-crypto-signals\",\"period\":\"1h\",\"currency\":\"PEPE\",\"price\":0.00000059,\"lossPrice\":0.00000055,\"gainPrices\":\"0.00000061,0.00000064\"}",
|
||||
"eventTime":1786600000000
|
||||
}`)
|
||||
_, data, err := Convert(body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
formatted, _ := data["formatted"].(string)
|
||||
for _, want := range []string{
|
||||
"开仓价格: 0.00000059\n",
|
||||
"止损价格: 0.00000055\n",
|
||||
} {
|
||||
if !strings.Contains(formatted, want) {
|
||||
t.Fatalf("missing %q in\n%s", want, formatted)
|
||||
}
|
||||
}
|
||||
if strings.Contains(formatted, "开仓价格: 0.00\n") {
|
||||
t.Fatalf("tiny price rounded to 0.00:\n%s", formatted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertInvalidJSON(t *testing.T) {
|
||||
_, _, err := Convert([]byte(`{not json`))
|
||||
if err == nil {
|
||||
|
||||
@@ -61,11 +61,14 @@ func (c *Converter) overrideFor(code string) *config.StrategyOverride {
|
||||
if c == nil || len(c.overrides) == 0 || code == "" {
|
||||
return nil
|
||||
}
|
||||
override, ok := c.overrides[code]
|
||||
if !ok {
|
||||
return nil
|
||||
// viper lower-cases nested map keys, so match case-insensitively.
|
||||
if override, ok := c.overrides[code]; ok {
|
||||
return &override
|
||||
}
|
||||
return &override
|
||||
if override, ok := c.overrides[strings.ToLower(code)]; ok {
|
||||
return &override
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func toData(body []byte, sig *Signal) (map[string]interface{}, error) {
|
||||
|
||||
@@ -36,6 +36,25 @@ func TestConvertOpen(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOverrideForLowercaseKey verifies overrides still match when viper
|
||||
// lower-cases the strategy_overrides map keys during config load.
|
||||
func TestOverrideForLowercaseKey(t *testing.T) {
|
||||
lev := 100
|
||||
c := NewConverter(map[string]config.StrategyOverride{
|
||||
"blong": {QuantityMultipliers: config.QuantityMultipliers{Add: 100}, Leverage: &lev},
|
||||
})
|
||||
o := c.overrideFor("BLONG")
|
||||
if o == nil {
|
||||
t.Fatal("overrideFor(BLONG)=nil, lowercase config key should match")
|
||||
}
|
||||
if got := o.QuantityMultiplierFor("ADD"); got != 100 {
|
||||
t.Fatalf("add multiplier=%v want 100", got)
|
||||
}
|
||||
if o.Leverage == nil || *o.Leverage != 100 {
|
||||
t.Fatalf("leverage=%v want 100", o.Leverage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertKeepsExtraJSONFields(t *testing.T) {
|
||||
_, data, err := NewConverter(nil).Convert([]byte(`{
|
||||
"action":"OPEN","symbol":"BTCUSDT","price":63014.61,
|
||||
|
||||
@@ -2,6 +2,8 @@ package tradesignal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aiaa-notification-service/internal/tz"
|
||||
@@ -29,7 +31,7 @@ func Format(signal *Signal, opts ...FormatOptions) string {
|
||||
action := strings.ToUpper(signal.Action)
|
||||
switch action {
|
||||
case "OPEN":
|
||||
lines = append(lines, fmt.Sprintf("开仓价格: %.2f", signal.Price))
|
||||
lines = append(lines, fmt.Sprintf("开仓价格: %s", formatPrice(signal.Price)))
|
||||
if line := sizeLine("OPEN", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
@@ -38,23 +40,23 @@ func Format(signal *Signal, opts ...FormatOptions) string {
|
||||
lines = append(lines, fmt.Sprintf("杠杆: %dx", signal.Leverage))
|
||||
}
|
||||
if signal.TakeProfitPrice != nil {
|
||||
lines = append(lines, fmt.Sprintf("止盈价格: %.2f", *signal.TakeProfitPrice))
|
||||
lines = append(lines, fmt.Sprintf("止盈价格: %s", formatPrice(*signal.TakeProfitPrice)))
|
||||
}
|
||||
if signal.StopLossPrice != nil {
|
||||
lines = append(lines, fmt.Sprintf("止损价格: %.2f", *signal.StopLossPrice))
|
||||
lines = append(lines, fmt.Sprintf("止损价格: %s", formatPrice(*signal.StopLossPrice)))
|
||||
}
|
||||
case "CLOSE":
|
||||
lines = append(lines, fmt.Sprintf("平仓价格: %.2f", signal.Price))
|
||||
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("平仓盈亏: %.2f", *signal.PnL))
|
||||
lines = append(lines, fmt.Sprintf("平仓盈亏: %s", formatPrice(*signal.PnL)))
|
||||
}
|
||||
if signal.AccountBalance != nil {
|
||||
lines = append(lines, fmt.Sprintf("账户余额:%.2f", *signal.AccountBalance))
|
||||
lines = append(lines, fmt.Sprintf("账户余额:%s", formatPrice(*signal.AccountBalance)))
|
||||
}
|
||||
case "ADD":
|
||||
lines = append(lines, fmt.Sprintf("加仓价格: %.2f", signal.Price))
|
||||
lines = append(lines, fmt.Sprintf("加仓价格: %s", formatPrice(signal.Price)))
|
||||
if line := sizeLine("ADD", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
@@ -63,19 +65,19 @@ func Format(signal *Signal, opts ...FormatOptions) string {
|
||||
lines = append(lines, fmt.Sprintf("杠杆: %dx", signal.Leverage))
|
||||
}
|
||||
case "REDUCE":
|
||||
lines = append(lines, fmt.Sprintf("减仓价格: %.2f", signal.Price))
|
||||
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("减仓盈亏: %.2f", *signal.PnL))
|
||||
lines = append(lines, fmt.Sprintf("减仓盈亏: %s", formatPrice(*signal.PnL)))
|
||||
}
|
||||
if signal.AccountBalance != nil {
|
||||
lines = append(lines, fmt.Sprintf("账户余额:%.2f", *signal.AccountBalance))
|
||||
lines = append(lines, fmt.Sprintf("账户余额:%s", formatPrice(*signal.AccountBalance)))
|
||||
}
|
||||
default:
|
||||
lines = append(lines, fmt.Sprintf("价格: %.2f", signal.Price))
|
||||
lines = append(lines, fmt.Sprintf("价格: %s", formatPrice(signal.Price)))
|
||||
if line := sizeLine("", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
@@ -127,7 +129,7 @@ func appendAvgPrice(lines []string, avgPrice *float64) []string {
|
||||
if avgPrice == nil || *avgPrice <= 0 {
|
||||
return lines
|
||||
}
|
||||
return append(lines, fmt.Sprintf("平均单价: %.2f", *avgPrice))
|
||||
return append(lines, fmt.Sprintf("平均单价: %s", formatPrice(*avgPrice)))
|
||||
}
|
||||
|
||||
func closeSizeLine(quantity, posMarginRatio *float64) string {
|
||||
@@ -188,6 +190,19 @@ 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"} {
|
||||
|
||||
@@ -58,3 +58,24 @@ func TestFormatWithAvgPrice(t *testing.T) {
|
||||
t.Fatalf("%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatTinyPriceKeepsPrecision(t *testing.T) {
|
||||
tp, sl := 0.00000061, 0.00000058
|
||||
out := Format(&Signal{
|
||||
Symbol: "PEPEUSDT", Side: "LONG", Action: "OPEN",
|
||||
Price: 0.00000059, TakeProfitPrice: &tp, StopLossPrice: &sl,
|
||||
EventTime: "2026-06-23T01:30:00Z",
|
||||
})
|
||||
for _, want := range []string{
|
||||
"开仓价格: 0.00000059\n",
|
||||
"止盈价格: 0.00000061\n",
|
||||
"止损价格: 0.00000058\n",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("missing %q in\n%s", want, out)
|
||||
}
|
||||
}
|
||||
if strings.Contains(out, "开仓价格: 0.00\n") {
|
||||
t.Fatalf("tiny price rounded to 0.00:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user