From 8e6a1332f0e412e0b7f90d1c07dbab2e717ebadf Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 10 Oct 2026 00:48:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E4=BA=A4=E6=98=93=E4=BF=A1=E5=8F=B7):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9C=AA=E8=BF=BD=E8=B8=AA=E4=BB=93=E4=BD=8D?= =?UTF-8?q?=E6=97=B6=E5=B9=B3=E5=9D=87=E5=8D=95=E4=BB=B7=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E4=B8=BA=20%!f()=20=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 本地无均价记录时回退使用上游 totalAvgPx - 跟单模板在无均价时不再输出平均单价行 --- docs/httpie/curls.md | 2 +- internal/engine/renderer_test.go | 2 +- internal/subscriber/tradesignal/convert.go | 19 ++++++-- .../subscriber/tradesignal/convert_test.go | 46 ++++++++++++++++++- internal/subscriber/tradesignal/signal.go | 1 + 5 files changed, 62 insertions(+), 8 deletions(-) diff --git a/docs/httpie/curls.md b/docs/httpie/curls.md index 93cc6d2..d5def19 100644 --- a/docs/httpie/curls.md +++ b/docs/httpie/curls.md @@ -696,7 +696,7 @@ curl -X POST 'http://localhost:8080/api/v1/templates' \ -H 'Content-Type: application/json' \ -d '{ "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}}" }' ``` diff --git a/internal/engine/renderer_test.go b/internal/engine/renderer_test.go index b35d05d..ce5ebe0 100644 --- a/internal/engine/renderer_test.go +++ b/internal/engine/renderer_test.go @@ -124,7 +124,7 @@ 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}}" +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) { tmpl := copyTradeTmpl diff --git a/internal/subscriber/tradesignal/convert.go b/internal/subscriber/tradesignal/convert.go index 1bed335..1763d58 100644 --- a/internal/subscriber/tradesignal/convert.go +++ b/internal/subscriber/tradesignal/convert.go @@ -50,8 +50,7 @@ func (c *Converter) Convert(body []byte) (string, map[string]interface{}, error) return "", nil, fmt.Errorf("%w: %v", ErrPositionStore, err) } var opts FormatOptions - if snap.HasAvg { - avg := snap.AvgPrice + if avg, ok := avgPriceFor(snap, out); ok { opts.AvgPrice = &avg } text := Format(out, opts) @@ -60,12 +59,24 @@ func (c *Converter) Convert(body []byte) (string, map[string]interface{}, error) return "", nil, err } data["formatted"] = text - if snap.HasAvg { - data["avgPrice"] = snap.AvgPrice + if opts.AvgPrice != nil { + data["avgPrice"] = *opts.AvgPrice } 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 { if c == nil || len(c.overrides) == 0 || code == "" { return nil diff --git a/internal/subscriber/tradesignal/convert_test.go b/internal/subscriber/tradesignal/convert_test.go index b2f7199..f4821ef 100644 --- a/internal/subscriber/tradesignal/convert_test.go +++ b/internal/subscriber/tradesignal/convert_test.go @@ -117,8 +117,7 @@ func TestConvertThenRenderCopyTradeTinyPEPE(t *testing.T) { } 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) + out, err := engine.NewRenderer().Render(copyTradeTmpl, data) if err != nil { t.Fatal(err) } @@ -138,3 +137,46 @@ func TestConvertThenRenderCopyTradeTinyPEPE(t *testing.T) { 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}}" diff --git a/internal/subscriber/tradesignal/signal.go b/internal/subscriber/tradesignal/signal.go index 8c8cb4f..bdb682c 100644 --- a/internal/subscriber/tradesignal/signal.go +++ b/internal/subscriber/tradesignal/signal.go @@ -22,6 +22,7 @@ type Signal struct { StopLossRatio *float64 `json:"stopLossRatio"` PnL *float64 `json:"pnl"` AccountBalance *float64 `json:"accountBalance"` + TotalAvgPx *float64 `json:"totalAvgPx"` RawMessage string `json:"rawMessage"` }