fix(交易信号): 修复未追踪仓位时平均单价渲染为 %!f(<nil>) 的问题

- 本地无均价记录时回退使用上游 totalAvgPx
- 跟单模板在无均价时不再输出平均单价行
This commit is contained in:
2026-10-10 00:48:35 +08:00
parent 931130038b
commit 8e6a1332f0
5 changed files with 62 additions and 8 deletions
+1 -1
View File
@@ -696,7 +696,7 @@ curl -X POST 'http://localhost:8080/api/v1/templates' \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{ -d '{
"name": "跟单策略", "name": "跟单策略",
"content": "{{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}}" "content": "{{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{{if .avgPrice}}平均单价: {{printf \"%.2f\" .avgPrice}}\n{{end}}{{if or (eq .action \"OPEN\") (eq .action \"ADD\")}}{{if .leverage}}杠杆: {{.leverage}}x\n{{end}}{{end}}策略: {{case .strategyCode \"BLONG\" \"B龙策略\" .strategyCode}}\n推送时间: {{.pushedAt}}"
}' }'
``` ```
+1 -1
View File
@@ -124,7 +124,7 @@ func TestRendererCaseDefault(t *testing.T) {
// copyTradeTmpl is the production 跟单策略 template. It formats prices with // copyTradeTmpl is the production 跟单策略 template. It formats prices with
// printf "%.2f", which must not collapse meme-coin prices like PEPE to 0.00. // 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}}" 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{{if .avgPrice}}平均单价: {{printf \"%.2f\" .avgPrice}}\n{{end}}{{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 := copyTradeTmpl tmpl := copyTradeTmpl
+15 -4
View File
@@ -50,8 +50,7 @@ func (c *Converter) Convert(body []byte) (string, map[string]interface{}, error)
return "", nil, fmt.Errorf("%w: %v", ErrPositionStore, err) return "", nil, fmt.Errorf("%w: %v", ErrPositionStore, err)
} }
var opts FormatOptions var opts FormatOptions
if snap.HasAvg { if avg, ok := avgPriceFor(snap, out); ok {
avg := snap.AvgPrice
opts.AvgPrice = &avg opts.AvgPrice = &avg
} }
text := Format(out, opts) text := Format(out, opts)
@@ -60,12 +59,24 @@ func (c *Converter) Convert(body []byte) (string, map[string]interface{}, error)
return "", nil, err return "", nil, err
} }
data["formatted"] = text data["formatted"] = text
if snap.HasAvg { if opts.AvgPrice != nil {
data["avgPrice"] = snap.AvgPrice data["avgPrice"] = *opts.AvgPrice
} }
return "trade." + strings.ToLower(out.Action), data, nil return "trade." + strings.ToLower(out.Action), data, nil
} }
// avgPriceFor prefers the tracked average and falls back to the upstream
// totalAvgPx when the tracker never saw the position open.
func avgPriceFor(snap Snapshot, sig *Signal) (float64, bool) {
if snap.HasAvg {
return snap.AvgPrice, true
}
if sig.TotalAvgPx != nil && *sig.TotalAvgPx > 0 {
return *sig.TotalAvgPx, true
}
return 0, false
}
func (c *Converter) overrideFor(code string) *config.StrategyOverride { func (c *Converter) overrideFor(code string) *config.StrategyOverride {
if c == nil || len(c.overrides) == 0 || code == "" { if c == nil || len(c.overrides) == 0 || code == "" {
return nil return nil
@@ -117,8 +117,7 @@ func TestConvertThenRenderCopyTradeTinyPEPE(t *testing.T) {
} }
data["pushedAt"] = "2026.08.22 10:04:29" 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(copyTradeTmpl, data)
out, err := engine.NewRenderer().Render(tmpl, data)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -138,3 +137,46 @@ func TestConvertThenRenderCopyTradeTinyPEPE(t *testing.T) {
t.Errorf("tiny price collapsed to 0.00:\n%s", out) t.Errorf("tiny price collapsed to 0.00:\n%s", out)
} }
} }
func TestConvertUntrackedReduceFallsBackToTotalAvgPx(t *testing.T) {
_, data, err := NewConverter(nil).Convert([]byte(`{
"signalId":"u1","strategyCode":"ALLINETH","symbol":"ETHUSDT",
"side":"SHORT","action":"REDUCE","quantity":0.11,"price":1890,
"totalAvgPx":1899.03,"posMarginRatio":0.5
}`))
if err != nil {
t.Fatal(err)
}
if data["avgPrice"] != 1899.03 {
t.Fatalf("avgPrice=%v want 1899.03", data["avgPrice"])
}
out, err := engine.NewRenderer().Render(copyTradeTmpl, data)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "平均单价: 1899.03") {
t.Fatalf("out=\n%s", out)
}
}
func TestConvertUntrackedCloseOmitsAvgPrice(t *testing.T) {
_, data, err := NewConverter(nil).Convert([]byte(`{
"signalId":"u2","strategyCode":"BLONG","symbol":"SOLUSDT",
"side":"SHORT","action":"CLOSE","quantity":0.12,"price":101.32,"totalAvgPx":0
}`))
if err != nil {
t.Fatal(err)
}
if _, ok := data["avgPrice"]; ok {
t.Fatalf("avgPrice should be absent, got %v", data["avgPrice"])
}
out, err := engine.NewRenderer().Render(copyTradeTmpl, data)
if err != nil {
t.Fatal(err)
}
if strings.Contains(out, "%!") || strings.Contains(out, "平均单价") {
t.Fatalf("out=\n%s", out)
}
}
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{{if .avgPrice}}平均单价: {{printf \"%.2f\" .avgPrice}}\n{{end}}{{if or (eq .action \"OPEN\") (eq .action \"ADD\")}}{{if .leverage}}杠杆: {{.leverage}}x\n{{end}}{{end}}策略: {{case .strategyCode \"BLONG\" \"B龙策略\" .strategyCode}}\n推送时间: {{.pushedAt}}"
@@ -22,6 +22,7 @@ type Signal struct {
StopLossRatio *float64 `json:"stopLossRatio"` StopLossRatio *float64 `json:"stopLossRatio"`
PnL *float64 `json:"pnl"` PnL *float64 `json:"pnl"`
AccountBalance *float64 `json:"accountBalance"` AccountBalance *float64 `json:"accountBalance"`
TotalAvgPx *float64 `json:"totalAvgPx"`
RawMessage string `json:"rawMessage"` RawMessage string `json:"rawMessage"`
} }