fix(价格展示): 修复模板渲染时极小价格被折叠为 0.00 的问题
Motivation: 此前的修复仅覆盖直接拼接文案的场景,跟单等基于模板渲染的推送场景中,%.2f 格式化仍会把 PEPE 等 Meme 币的极小价格(如 0.00000059)折叠为 0.00,导致交易通知丢失真实价格、误导用户。 Changes: * 抽取共享的价格显示逻辑到统一显示层,两个订阅者改为复用,移除重复实现 * 新增 Decimal 类型,渲染时以副本方式包装模板数据,使模板内 printf 风格格式化保留极小价格精度且不出现科学计数法,同时不修改调用方原始数据 * 渲染引擎空值判断改用反射实现,覆盖全部整型、无符号整型与浮点类型 * 补充转换到渲染的端到端回归测试,覆盖极小价格精度、科学计数法与数据不可变性
This commit is contained in:
+14
-11
@@ -3,8 +3,11 @@ package engine
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"aiaa-notification-service/internal/display"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Renderer struct{}
|
type Renderer struct{}
|
||||||
@@ -26,7 +29,7 @@ func (r *Renderer) Render(tmplContent string, data map[string]interface{}) (stri
|
|||||||
return "", fmt.Errorf("parse template: %w", err)
|
return "", fmt.Errorf("parse template: %w", err)
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := tmpl.Execute(&buf, data); err != nil {
|
if err := tmpl.Execute(&buf, display.WrapMap(data)); err != nil {
|
||||||
return "", fmt.Errorf("execute template: %w", err)
|
return "", fmt.Errorf("execute template: %w", err)
|
||||||
}
|
}
|
||||||
return buf.String(), nil
|
return buf.String(), nil
|
||||||
@@ -82,17 +85,17 @@ func isEmptyValue(v any) bool {
|
|||||||
return strings.TrimSpace(t) == ""
|
return strings.TrimSpace(t) == ""
|
||||||
case bool:
|
case bool:
|
||||||
return !t
|
return !t
|
||||||
case int:
|
default:
|
||||||
return t == 0
|
rv := reflect.ValueOf(v)
|
||||||
case int32:
|
switch rv.Kind() {
|
||||||
return t == 0
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
case int64:
|
return rv.Int() == 0
|
||||||
return t == 0
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
case float32:
|
return rv.Uint() == 0
|
||||||
return t == 0
|
case reflect.Float32, reflect.Float64:
|
||||||
case float64:
|
return rv.Float() == 0
|
||||||
return t == 0
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func TestRendererLineOmitsEmpty(t *testing.T) {
|
|||||||
"symbol": "ICP",
|
"symbol": "ICP",
|
||||||
"price": 2.273,
|
"price": 2.273,
|
||||||
"totalAvgPx": "",
|
"totalAvgPx": "",
|
||||||
"stopLossPrice": 0,
|
"stopLossPrice": float64(0),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -122,8 +122,12 @@ func TestRendererCaseDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copyTradeTmpl is the production 跟单策略 template. It formats prices with
|
||||||
|
// printf "%.2f", which must not collapse meme-coin prices like PEPE to 0.00.
|
||||||
|
const copyTradeTmpl = "{{case .side \"LONG\" \"多单\" \"SHORT\" \"空单\"}}{{case .action \"OPEN\" \"开仓\" \"ADD\" \"加仓\" \"CLOSE\" \"平仓\" \"REDUCE\" \"减仓\"}}\n交易品种: {{case .symbol \"ETHUSDT\" \"ETH\" \"BTCUSDT\" \"BTC\" \"SOLUSDT\" \"SOL\" \"BNBUSDT\" \"BNB\" .symbol}}\n{{case .action \"OPEN\" \"开仓价格\" \"ADD\" \"加仓价格\" \"CLOSE\" \"平仓价格\" \"REDUCE\" \"减仓价格\"}}: {{printf \"%.2f\" .price}}\n{{case .action \"OPEN\" \"开仓数量\" \"ADD\" \"加仓数量\" \"CLOSE\" \"平仓数量\" \"REDUCE\" \"减仓数量\"}}: {{printf \"%.2f\" .quantity}}\n平均单价: {{printf \"%.2f\" .avgPrice}}\n{{if or (eq .action \"OPEN\") (eq .action \"ADD\")}}{{if .leverage}}杠杆: {{.leverage}}x\n{{end}}{{end}}策略: {{case .strategyCode \"BLONG\" \"B龙策略\" .strategyCode}}\n推送时间: {{.pushedAt}}"
|
||||||
|
|
||||||
func TestCopyTradeTemplate(t *testing.T) {
|
func TestCopyTradeTemplate(t *testing.T) {
|
||||||
tmpl := "{{case .side \"LONG\" \"多单\" \"SHORT\" \"空单\"}}{{case .action \"OPEN\" \"开仓\" \"ADD\" \"加仓\" \"CLOSE\" \"平仓\" \"REDUCE\" \"减仓\"}}\n交易品种: {{case .symbol \"ETHUSDT\" \"ETH\" \"BTCUSDT\" \"BTC\" \"SOLUSDT\" \"SOL\" \"BNBUSDT\" \"BNB\" .symbol}}\n{{case .action \"OPEN\" \"开仓价格\" \"ADD\" \"加仓价格\" \"CLOSE\" \"平仓价格\" \"REDUCE\" \"减仓价格\"}}: {{printf \"%.2f\" .price}}\n{{case .action \"OPEN\" \"开仓数量\" \"ADD\" \"加仓数量\" \"CLOSE\" \"平仓数量\" \"REDUCE\" \"减仓数量\"}}: {{printf \"%.2f\" .quantity}}\n平均单价: {{printf \"%.2f\" .avgPrice}}\n{{if or (eq .action \"OPEN\") (eq .action \"ADD\")}}{{if .leverage}}杠杆: {{.leverage}}x\n{{end}}{{end}}策略: {{case .strategyCode \"BLONG\" \"B龙策略\" .strategyCode}}\n推送时间: {{.pushedAt}}"
|
tmpl := copyTradeTmpl
|
||||||
r := NewRenderer()
|
r := NewRenderer()
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -169,6 +173,21 @@ func TestCopyTradeTemplate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: []string{"空单加仓", "加仓价格: 1933.05", "加仓数量: 3.80", "杠杆: 100x", "策略: B龙策略", "推送时间: 2026.08.10 06:14:28"},
|
want: []string{"空单加仓", "加仓价格: 1933.05", "加仓数量: 3.80", "杠杆: 100x", "策略: B龙策略", "推送时间: 2026.08.10 06:14:28"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "pepe-open-keeps-tiny-price",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"side": "SHORT", "action": "OPEN", "symbol": "PEPEUSDT",
|
||||||
|
"price": 0.00000059, "quantity": 1000000000.0, "avgPrice": 0.00000059,
|
||||||
|
"leverage": 100, "strategyCode": "BLONG", "pushedAt": "2026.08.22 10:04:29",
|
||||||
|
},
|
||||||
|
want: []string{
|
||||||
|
"空单开仓", "交易品种: PEPEUSDT",
|
||||||
|
"开仓价格: 0.00000059", "开仓数量: 1000000000.00",
|
||||||
|
"平均单价: 0.00000059", "杠杆: 100x",
|
||||||
|
"策略: B龙策略", "推送时间: 2026.08.22 10:04:29",
|
||||||
|
},
|
||||||
|
not: []string{"开仓价格: 0.00\n", "平均单价: 0.00\n"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@@ -190,3 +209,30 @@ func TestCopyTradeTemplate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRendererTinyPriceNotScientificOrRounded(t *testing.T) {
|
||||||
|
r := NewRenderer()
|
||||||
|
data := map[string]interface{}{"price": 0.00000059, "stopLossPrice": 0.00000055}
|
||||||
|
out, err := r.Render(`价格: {{.price}}
|
||||||
|
{{line "止损价" .stopLossPrice}}`, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(out, "e-") || strings.Contains(out, "E-") {
|
||||||
|
t.Fatalf("tiny price lost precision:\n%s", out)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, "价格: 0.00000059") || !strings.Contains(out, "止损价:0.00000055") {
|
||||||
|
t.Fatalf("out=%q", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRendererDoesNotMutateData(t *testing.T) {
|
||||||
|
r := NewRenderer()
|
||||||
|
data := map[string]interface{}{"price": 0.00000059}
|
||||||
|
if _, err := r.Render(`{{printf "%.2f" .price}}`, data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, ok := data["price"].(float64); !ok {
|
||||||
|
t.Fatalf("render mutated caller data: %T", data["price"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"aiaa-notification-service/internal/display"
|
||||||
"aiaa-notification-service/internal/tz"
|
"aiaa-notification-service/internal/tz"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -245,23 +245,23 @@ func format(env envelope, p payload, action string) string {
|
|||||||
switch action {
|
switch action {
|
||||||
case "CLOSE":
|
case "CLOSE":
|
||||||
if p.Price > 0 {
|
if p.Price > 0 {
|
||||||
lines = append(lines, fmt.Sprintf("平仓价格: %s", formatPrice(p.Price)))
|
lines = append(lines, fmt.Sprintf("平仓价格: %s", display.FormatPrice(p.Price)))
|
||||||
}
|
}
|
||||||
case "GAIN":
|
case "GAIN":
|
||||||
if p.Price > 0 {
|
if p.Price > 0 {
|
||||||
lines = append(lines, fmt.Sprintf("止盈价格: %s", formatPrice(p.Price)))
|
lines = append(lines, fmt.Sprintf("止盈价格: %s", display.FormatPrice(p.Price)))
|
||||||
}
|
}
|
||||||
case "SELL":
|
case "SELL":
|
||||||
if p.Price > 0 {
|
if p.Price > 0 {
|
||||||
lines = append(lines, fmt.Sprintf("卖出价格: %s", formatPrice(p.Price)))
|
lines = append(lines, fmt.Sprintf("卖出价格: %s", display.FormatPrice(p.Price)))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if p.Price > 0 {
|
if p.Price > 0 {
|
||||||
lines = append(lines, fmt.Sprintf("开仓价格: %s", formatPrice(p.Price)))
|
lines = append(lines, fmt.Sprintf("开仓价格: %s", display.FormatPrice(p.Price)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p.LossPrice > 0 {
|
if p.LossPrice > 0 {
|
||||||
lines = append(lines, fmt.Sprintf("止损价格: %s", formatPrice(p.LossPrice)))
|
lines = append(lines, fmt.Sprintf("止损价格: %s", display.FormatPrice(p.LossPrice)))
|
||||||
}
|
}
|
||||||
if er := entryRange(p.Price, p.OpenPrice2); er != "" && p.OpenPrice2 != 0 {
|
if er := entryRange(p.Price, p.OpenPrice2); er != "" && p.OpenPrice2 != 0 {
|
||||||
lines = append(lines, fmt.Sprintf("介入区间: %s", er))
|
lines = append(lines, fmt.Sprintf("介入区间: %s", er))
|
||||||
@@ -360,19 +360,6 @@ func formatFloat(f float64) string {
|
|||||||
return strconv.FormatFloat(f, 'f', -1, 64)
|
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 {
|
func firstNonEmpty(a, b string) string {
|
||||||
if strings.TrimSpace(a) != "" {
|
if strings.TrimSpace(a) != "" {
|
||||||
return a
|
return a
|
||||||
|
|||||||
@@ -272,6 +272,18 @@ func TestConvertTinyPriceKeepsPrecision(t *testing.T) {
|
|||||||
if strings.Contains(formatted, "开仓价格: 0.00\n") {
|
if strings.Contains(formatted, "开仓价格: 0.00\n") {
|
||||||
t.Fatalf("tiny price rounded to 0.00:\n%s", formatted)
|
t.Fatalf("tiny price rounded to 0.00:\n%s", formatted)
|
||||||
}
|
}
|
||||||
|
out, err := engine.NewRenderer().Render("价格:{{.price}}\n止损:{{.stopLossPrice}}\n{{line \"止盈\" .takeProfitPrice}}", data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{"价格:0.00000059", "止损:0.00000055"} {
|
||||||
|
if !strings.Contains(out, want) {
|
||||||
|
t.Fatalf("missing %q in rendered\n%s", want, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(out, "e-") || strings.Contains(out, "E-") {
|
||||||
|
t.Fatalf("scientific notation in rendered\n%s", out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConvertInvalidJSON(t *testing.T) {
|
func TestConvertInvalidJSON(t *testing.T) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"aiaa-notification-service/internal/config"
|
"aiaa-notification-service/internal/config"
|
||||||
|
"aiaa-notification-service/internal/engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConvertOpen(t *testing.T) {
|
func TestConvertOpen(t *testing.T) {
|
||||||
@@ -97,3 +98,43 @@ func TestConvertRawMessageWithoutAction(t *testing.T) {
|
|||||||
t.Fatalf("data=%v", data)
|
t.Fatalf("data=%v", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConvertThenRenderCopyTradeTinyPEPE(t *testing.T) {
|
||||||
|
lev := 100
|
||||||
|
c := NewConverter(map[string]config.StrategyOverride{
|
||||||
|
"BLONG": {QuantityMultipliers: config.QuantityMultipliers{Open: 100}, Leverage: &lev},
|
||||||
|
})
|
||||||
|
_, data, err := c.Convert([]byte(`{
|
||||||
|
"signalId":"s1","strategyCode":"BLONG","symbol":"PEPEUSDT",
|
||||||
|
"side":"SHORT","action":"OPEN","quantity":10000000,"price":0.00000059,
|
||||||
|
"leverage":10,"eventTime":"2026-08-22T02:04:29Z"
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if p, ok := data["price"].(float64); !ok || p != 0.00000059 {
|
||||||
|
t.Fatalf("convert price=%v (%T), want float64 0.00000059", data["price"], data["price"])
|
||||||
|
}
|
||||||
|
data["pushedAt"] = "2026.08.22 10:04:29"
|
||||||
|
|
||||||
|
const tmpl = "{{case .side \"LONG\" \"多单\" \"SHORT\" \"空单\"}}{{case .action \"OPEN\" \"开仓\" \"ADD\" \"加仓\" \"CLOSE\" \"平仓\" \"REDUCE\" \"减仓\"}}\n交易品种: {{case .symbol \"ETHUSDT\" \"ETH\" \"BTCUSDT\" \"BTC\" \"SOLUSDT\" \"SOL\" \"BNBUSDT\" \"BNB\" .symbol}}\n{{case .action \"OPEN\" \"开仓价格\" \"ADD\" \"加仓价格\" \"CLOSE\" \"平仓价格\" \"REDUCE\" \"减仓价格\"}}: {{printf \"%.2f\" .price}}\n{{case .action \"OPEN\" \"开仓数量\" \"ADD\" \"加仓数量\" \"CLOSE\" \"平仓数量\" \"REDUCE\" \"减仓数量\"}}: {{printf \"%.2f\" .quantity}}\n平均单价: {{printf \"%.2f\" .avgPrice}}\n{{if or (eq .action \"OPEN\") (eq .action \"ADD\")}}{{if .leverage}}杠杆: {{.leverage}}x\n{{end}}{{end}}策略: {{case .strategyCode \"BLONG\" \"B龙策略\" .strategyCode}}\n推送时间: {{.pushedAt}}"
|
||||||
|
out, err := engine.NewRenderer().Render(tmpl, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, want := range []string{
|
||||||
|
"空单开仓",
|
||||||
|
"交易品种: PEPEUSDT",
|
||||||
|
"开仓价格: 0.00000059",
|
||||||
|
"开仓数量: 1000000000.00",
|
||||||
|
"平均单价: 0.00000059",
|
||||||
|
"杠杆: 100x",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(out, want) {
|
||||||
|
t.Errorf("missing %q in\n%s", want, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(out, "开仓价格: 0.00\n") || strings.Contains(out, "平均单价: 0.00\n") {
|
||||||
|
t.Errorf("tiny price collapsed to 0.00:\n%s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package tradesignal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"aiaa-notification-service/internal/display"
|
||||||
"aiaa-notification-service/internal/tz"
|
"aiaa-notification-service/internal/tz"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ func Format(signal *Signal, opts ...FormatOptions) string {
|
|||||||
action := strings.ToUpper(signal.Action)
|
action := strings.ToUpper(signal.Action)
|
||||||
switch action {
|
switch action {
|
||||||
case "OPEN":
|
case "OPEN":
|
||||||
lines = append(lines, fmt.Sprintf("开仓价格: %s", formatPrice(signal.Price)))
|
lines = append(lines, fmt.Sprintf("开仓价格: %s", display.FormatPrice(signal.Price)))
|
||||||
if line := sizeLine("OPEN", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
if line := sizeLine("OPEN", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
||||||
lines = append(lines, line)
|
lines = append(lines, line)
|
||||||
}
|
}
|
||||||
@@ -40,23 +39,23 @@ func Format(signal *Signal, opts ...FormatOptions) string {
|
|||||||
lines = append(lines, fmt.Sprintf("杠杆: %dx", signal.Leverage))
|
lines = append(lines, fmt.Sprintf("杠杆: %dx", signal.Leverage))
|
||||||
}
|
}
|
||||||
if signal.TakeProfitPrice != nil {
|
if signal.TakeProfitPrice != nil {
|
||||||
lines = append(lines, fmt.Sprintf("止盈价格: %s", formatPrice(*signal.TakeProfitPrice)))
|
lines = append(lines, fmt.Sprintf("止盈价格: %s", display.FormatPrice(*signal.TakeProfitPrice)))
|
||||||
}
|
}
|
||||||
if signal.StopLossPrice != nil {
|
if signal.StopLossPrice != nil {
|
||||||
lines = append(lines, fmt.Sprintf("止损价格: %s", formatPrice(*signal.StopLossPrice)))
|
lines = append(lines, fmt.Sprintf("止损价格: %s", display.FormatPrice(*signal.StopLossPrice)))
|
||||||
}
|
}
|
||||||
case "CLOSE":
|
case "CLOSE":
|
||||||
lines = append(lines, fmt.Sprintf("平仓价格: %s", formatPrice(signal.Price)))
|
lines = append(lines, fmt.Sprintf("平仓价格: %s", display.FormatPrice(signal.Price)))
|
||||||
lines = append(lines, closeSizeLine(signal.Quantity, signal.PosMarginRatio))
|
lines = append(lines, closeSizeLine(signal.Quantity, signal.PosMarginRatio))
|
||||||
lines = appendAvgPrice(lines, opt.AvgPrice)
|
lines = appendAvgPrice(lines, opt.AvgPrice)
|
||||||
if signal.PnL != nil {
|
if signal.PnL != nil {
|
||||||
lines = append(lines, fmt.Sprintf("平仓盈亏: %s", formatPrice(*signal.PnL)))
|
lines = append(lines, fmt.Sprintf("平仓盈亏: %s", display.FormatPrice(*signal.PnL)))
|
||||||
}
|
}
|
||||||
if signal.AccountBalance != nil {
|
if signal.AccountBalance != nil {
|
||||||
lines = append(lines, fmt.Sprintf("账户余额:%s", formatPrice(*signal.AccountBalance)))
|
lines = append(lines, fmt.Sprintf("账户余额:%s", display.FormatPrice(*signal.AccountBalance)))
|
||||||
}
|
}
|
||||||
case "ADD":
|
case "ADD":
|
||||||
lines = append(lines, fmt.Sprintf("加仓价格: %s", formatPrice(signal.Price)))
|
lines = append(lines, fmt.Sprintf("加仓价格: %s", display.FormatPrice(signal.Price)))
|
||||||
if line := sizeLine("ADD", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
if line := sizeLine("ADD", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
||||||
lines = append(lines, line)
|
lines = append(lines, line)
|
||||||
}
|
}
|
||||||
@@ -65,19 +64,19 @@ func Format(signal *Signal, opts ...FormatOptions) string {
|
|||||||
lines = append(lines, fmt.Sprintf("杠杆: %dx", signal.Leverage))
|
lines = append(lines, fmt.Sprintf("杠杆: %dx", signal.Leverage))
|
||||||
}
|
}
|
||||||
case "REDUCE":
|
case "REDUCE":
|
||||||
lines = append(lines, fmt.Sprintf("减仓价格: %s", formatPrice(signal.Price)))
|
lines = append(lines, fmt.Sprintf("减仓价格: %s", display.FormatPrice(signal.Price)))
|
||||||
if line := sizeLine("REDUCE", signal.Quantity, signal.PosMarginRatio); line != "" {
|
if line := sizeLine("REDUCE", signal.Quantity, signal.PosMarginRatio); line != "" {
|
||||||
lines = append(lines, line)
|
lines = append(lines, line)
|
||||||
}
|
}
|
||||||
lines = appendAvgPrice(lines, opt.AvgPrice)
|
lines = appendAvgPrice(lines, opt.AvgPrice)
|
||||||
if signal.PnL != nil {
|
if signal.PnL != nil {
|
||||||
lines = append(lines, fmt.Sprintf("减仓盈亏: %s", formatPrice(*signal.PnL)))
|
lines = append(lines, fmt.Sprintf("减仓盈亏: %s", display.FormatPrice(*signal.PnL)))
|
||||||
}
|
}
|
||||||
if signal.AccountBalance != nil {
|
if signal.AccountBalance != nil {
|
||||||
lines = append(lines, fmt.Sprintf("账户余额:%s", formatPrice(*signal.AccountBalance)))
|
lines = append(lines, fmt.Sprintf("账户余额:%s", display.FormatPrice(*signal.AccountBalance)))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
lines = append(lines, fmt.Sprintf("价格: %s", formatPrice(signal.Price)))
|
lines = append(lines, fmt.Sprintf("价格: %s", display.FormatPrice(signal.Price)))
|
||||||
if line := sizeLine("", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
if line := sizeLine("", signal.Quantity, signal.AmountMarginRatio); line != "" {
|
||||||
lines = append(lines, line)
|
lines = append(lines, line)
|
||||||
}
|
}
|
||||||
@@ -129,7 +128,7 @@ func appendAvgPrice(lines []string, avgPrice *float64) []string {
|
|||||||
if avgPrice == nil || *avgPrice <= 0 {
|
if avgPrice == nil || *avgPrice <= 0 {
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
return append(lines, fmt.Sprintf("平均单价: %s", formatPrice(*avgPrice)))
|
return append(lines, fmt.Sprintf("平均单价: %s", display.FormatPrice(*avgPrice)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeSizeLine(quantity, posMarginRatio *float64) string {
|
func closeSizeLine(quantity, posMarginRatio *float64) string {
|
||||||
@@ -190,19 +189,6 @@ func formatPercent(ratio float64) string {
|
|||||||
return fmt.Sprintf("%.2f%%", ratio*100)
|
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 {
|
func trimQuote(symbol string) string {
|
||||||
symbol = strings.ToUpper(symbol)
|
symbol = strings.ToUpper(symbol)
|
||||||
for _, suffix := range []string{"USDT", "USDC", "BUSD", "USD"} {
|
for _, suffix := range []string{"USDT", "USDC", "BUSD", "USD"} {
|
||||||
|
|||||||
Reference in New Issue
Block a user