diff --git a/internal/helper/binancex/resolver.go b/internal/helper/binancex/resolver.go new file mode 100644 index 0000000..817a2be --- /dev/null +++ b/internal/helper/binancex/resolver.go @@ -0,0 +1,35 @@ +package binancex + +import ( + "strings" + + "me.thuanle/bbot/internal/data" +) + +func Token2FutureSymbols(token string) []string { + return Token2Symbols(token) +} + +func Token2SpotSymbols(token string) []string { + futureSymbols := Token2FutureSymbols(token) + spots := make([]string, 0, len(futureSymbols)+1) + seen := make(map[string]struct{}, len(futureSymbols)+1) + + for _, futureSymbol := range futureSymbols { + spotSymbol := Future2SpotSymbol(futureSymbol) + if _, ok := seen[spotSymbol]; ok { + continue + } + seen[spotSymbol] = struct{}{} + spots = append(spots, spotSymbol) + } + + spotOnly := strings.ToUpper(token) + "USDT" + if data.Market.IsSpotPair(spotOnly) { + if _, ok := seen[spotOnly]; !ok { + spots = append(spots, spotOnly) + } + } + + return spots +} diff --git a/internal/helper/binancex/resolver_test.go b/internal/helper/binancex/resolver_test.go new file mode 100644 index 0000000..5232a78 --- /dev/null +++ b/internal/helper/binancex/resolver_test.go @@ -0,0 +1,92 @@ +package binancex + +import ( + "testing" + + "me.thuanle/bbot/internal/data" + "me.thuanle/bbot/internal/data/market" +) + +type resolverMarketStub struct { + spotPairs map[string]bool + futuresPairs map[string]bool +} + +func (m *resolverMarketStub) GetFuturePrice(symbol string) (float64, float64, int64, bool) { + return 0, 0, 0, false +} +func (m *resolverMarketStub) GetAllPremiumIndex() (map[string]market.PremiumIndex, error) { + return nil, nil +} +func (m *resolverMarketStub) GetAllFundRate() (map[string]float64, map[string]int64) { return nil, nil } +func (m *resolverMarketStub) GetSpotPrice(symbol string) (float64, bool) { return 0, false } +func (m *resolverMarketStub) GetMarginInterestRates() map[string]float64 { return nil } +func (m *resolverMarketStub) IsAlphaToken(symbol string) bool { return false } +func (m *resolverMarketStub) GetAlphaToken(symbol string) (market.AlphaTokenInfo, bool) { + return market.AlphaTokenInfo{}, false +} +func (m *resolverMarketStub) GetAlphaPrice(symbol string) (float64, bool) { return 0, false } +func (m *resolverMarketStub) IsSpotPair(symbol string) bool { return m.spotPairs[symbol] } +func (m *resolverMarketStub) IsFuturesPair(symbol string) bool { return m.futuresPairs[symbol] } +func (m *resolverMarketStub) RefreshTradingPairCache() error { return nil } + +func TestToken2FutureSymbols_ResolvesPrefixAndSuffix(t *testing.T) { + orig := data.Market + defer func() { data.Market = orig }() + + data.Market = &resolverMarketStub{ + futuresPairs: map[string]bool{ + "1000PEPEUSDT": true, + "1000PEPEUSDC": true, + }, + } + + syms := Token2FutureSymbols("pepe") + if len(syms) == 0 { + t.Fatalf("expected future symbols for PEPE") + } + + foundUSDT := false + foundUSDC := false + for _, sym := range syms { + if sym == "1000PEPEUSDT" { + foundUSDT = true + } + if sym == "1000PEPEUSDC" { + foundUSDC = true + } + } + + if !foundUSDT || !foundUSDC { + t.Fatalf("expected both 1000PEPEUSDT and 1000PEPEUSDC, got %+v", syms) + } +} + +func TestToken2SpotSymbols_AppliesExplicitRemap(t *testing.T) { + orig := data.Market + defer func() { data.Market = orig }() + + data.Market = &resolverMarketStub{ + futuresPairs: map[string]bool{"LUNA2USDT": true}, + spotPairs: map[string]bool{"LUNAUSDT": true}, + } + + spots := Token2SpotSymbols("luna2") + if len(spots) != 1 || spots[0] != "LUNAUSDT" { + t.Fatalf("expected [LUNAUSDT], got %+v", spots) + } +} + +func TestToken2SpotSymbols_SpotOnlyFallback(t *testing.T) { + orig := data.Market + defer func() { data.Market = orig }() + + data.Market = &resolverMarketStub{ + spotPairs: map[string]bool{"ABCUSDT": true}, + } + + spots := Token2SpotSymbols("abc") + if len(spots) != 1 || spots[0] != "ABCUSDT" { + t.Fatalf("expected [ABCUSDT], got %+v", spots) + } +} diff --git a/internal/services/tele/commands/token.go b/internal/services/tele/commands/token.go index a1be9fa..7267264 100644 --- a/internal/services/tele/commands/token.go +++ b/internal/services/tele/commands/token.go @@ -6,6 +6,7 @@ import ( "gopkg.in/telebot.v3" "me.thuanle/bbot/internal/configs/tele" "me.thuanle/bbot/internal/data" + "me.thuanle/bbot/internal/helper/binancex" "me.thuanle/bbot/internal/services/tele/chat" "me.thuanle/bbot/internal/services/tele/view" ) @@ -75,21 +76,23 @@ func collectRichTokenData(token string) buildRichTokenMessageArgs { } } - futureSymbol := token + "USDT" - if data.Market.IsFuturesPair(futureSymbol) { + futureSymbols := binancex.Token2FutureSymbols(token) + for _, futureSymbol := range futureSymbols { if fp, fr, ft, ok := data.Market.GetFuturePrice(futureSymbol); ok { a.HasFuture = true a.FuturePrice = fp a.FundingRate = fr a.FundingTimeMs = ft + break } } - spotSymbol := token + "USDT" - if data.Market.IsSpotPair(spotSymbol) { + spotSymbols := binancex.Token2SpotSymbols(token) + for _, spotSymbol := range spotSymbols { if sp, ok := data.Market.GetSpotPrice(spotSymbol); ok { a.HasSpot = true a.SpotPrice = sp + break } } diff --git a/internal/services/tele/commands/token_test.go b/internal/services/tele/commands/token_test.go index e8082ef..496aaea 100644 --- a/internal/services/tele/commands/token_test.go +++ b/internal/services/tele/commands/token_test.go @@ -113,9 +113,15 @@ func TestCollectRichTokenData_FutureFailureStillKeepsSpot(t *testing.T) { defer func() { data.Market = orig }() data.Market = &marketStub{ - spotPairs: map[string]bool{"ETHUSDT": true}, - futuresPairs: map[string]bool{"ETHUSDT": true}, - spotPrices: map[string]float64{"ETHUSDT": 3245}, + spotPairs: map[string]bool{ + "ETHUSDT": true, + }, + futuresPairs: map[string]bool{ + "ETHUSDT": true, + }, + spotPrices: map[string]float64{ + "ETHUSDT": 3245, + }, futurePrices: map[string]struct { price float64 rate float64 @@ -132,6 +138,67 @@ func TestCollectRichTokenData_FutureFailureStillKeepsSpot(t *testing.T) { } } +func TestCollectRichTokenData_UsesSharedResolverMapping(t *testing.T) { + orig := data.Market + defer func() { data.Market = orig }() + + data.Market = &marketStub{ + spotPairs: map[string]bool{ + "LUNAUSDT": true, + }, + futuresPairs: map[string]bool{ + "LUNA2USDT": true, + }, + spotPrices: map[string]float64{ + "LUNAUSDT": 0.49, + }, + futurePrices: map[string]struct { + price float64 + rate float64 + time int64 + }{ + "LUNA2USDT": {price: 0.50, rate: 0.0001, time: 1740000000000}, + }, + } + + args := collectRichTokenData("LUNA2") + if !args.HasFuture || !args.HasSpot { + t.Fatalf("expected both future and mapped spot, got %+v", args) + } + if args.SpotPrice != 0.49 { + t.Fatalf("expected mapped spot price 0.49, got %f", args.SpotPrice) + } +} + +func TestCollectRichTokenData_PrefixFutureMapsToSpot(t *testing.T) { + orig := data.Market + defer func() { data.Market = orig }() + + data.Market = &marketStub{ + spotPairs: map[string]bool{ + "PEPEUSDT": true, + }, + futuresPairs: map[string]bool{ + "1000PEPEUSDT": true, + }, + spotPrices: map[string]float64{ + "PEPEUSDT": 0.000012, + }, + futurePrices: map[string]struct { + price float64 + rate float64 + time int64 + }{ + "1000PEPEUSDT": {price: 0.000013, rate: 0.0002, time: 1740000000000}, + }, + } + + args := collectRichTokenData("PEPE") + if !args.HasFuture || !args.HasSpot { + t.Fatalf("expected both future and mapped spot for prefixed contract, got %+v", args) + } +} + func TestCollectRichTokenData_AlphaUsesSymbolPrice(t *testing.T) { orig := data.Market defer func() { data.Market = orig }()