Files
aiaa-notification-server/internal/subscriber/tradesignal/convert_test.go
T
ryan 8e6a1332f0 fix(交易信号): 修复未追踪仓位时平均单价渲染为 %!f(<nil>) 的问题
- 本地无均价记录时回退使用上游 totalAvgPx
- 跟单模板在无均价时不再输出平均单价行
2026-10-10 00:48:35 +08:00

183 lines
6.1 KiB
Go

package tradesignal
import (
"errors"
"strings"
"testing"
"aiaa-notification-service/internal/config"
"aiaa-notification-service/internal/engine"
)
func TestConvertOpen(t *testing.T) {
lev := 100
c := NewConverter(map[string]config.StrategyOverride{
"BLONG": {QuantityMultipliers: config.QuantityMultipliers{Open: 100}, Leverage: &lev},
})
event, data, err := c.Convert([]byte(`{
"signalId":"s1","strategyCode":"BLONG","symbol":"BTCUSDT",
"side":"LONG","action":"OPEN","quantity":0.01,"price":64000,
"leverage":10,"period":"1h","eventTime":"2026-06-23T01:30:00Z"
}`))
if err != nil {
t.Fatal(err)
}
if event != "trade.open" {
t.Fatalf("event=%q", event)
}
formatted, _ := data["formatted"].(string)
if !strings.Contains(formatted, "周期: 1h") || !strings.Contains(formatted, "开仓数量: 1.00") {
t.Fatalf("formatted=\n%s", formatted)
}
if data["period"] != "1h" || data["strategyCode"] != "BLONG" {
t.Fatalf("data=%v", data)
}
if data["leverage"] != float64(100) && data["leverage"] != 100 {
t.Fatalf("leverage=%v", data["leverage"])
}
}
// 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,
"totalAvgPx":63014.61,"investmentAmount":100095.24
}`))
if err != nil {
t.Fatal(err)
}
if data["totalAvgPx"] != 63014.61 {
t.Fatalf("totalAvgPx=%v", data["totalAvgPx"])
}
}
func TestConvertInvalidJSON(t *testing.T) {
_, _, err := NewConverter(nil).Convert([]byte(`{`))
if !errors.Is(err, ErrInvalidSignal) {
t.Fatalf("err=%v", err)
}
}
func TestConvertMissingAction(t *testing.T) {
_, _, err := NewConverter(nil).Convert([]byte(`{"symbol":"BTCUSDT"}`))
if !errors.Is(err, ErrInvalidSignal) {
t.Fatalf("err=%v", err)
}
}
func TestConvertRawMessageWithoutAction(t *testing.T) {
event, data, err := NewConverter(nil).Convert([]byte(`{
"strategyCode":"PUTEJJ",
"rawMessage":"4.7.15~2026.8.15 本周期 10万本金 期末109640,盈利9.6%"
}`))
if err != nil {
t.Fatal(err)
}
if event != "trade.message" {
t.Fatalf("event=%q", event)
}
if data["rawMessage"] != "4.7.15~2026.8.15 本周期 10万本金 期末109640,盈利9.6%" {
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"
out, err := engine.NewRenderer().Render(copyTradeTmpl, 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)
}
}
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}}"