From 2c5a9e23bf5323ad79a4d1af3b00c7c572a6268c Mon Sep 17 00:00:00 2001 From: thuanle Date: Mon, 27 Apr 2026 03:31:48 +0700 Subject: [PATCH 1/2] refactor binance symbol resolver --- internal/configs/binance/asset.go | 3 ++ internal/helper/binancex/resolver.go | 38 +++++++++++++++++++- internal/helper/binancex/symbol.go | 52 +++------------------------- 3 files changed, 44 insertions(+), 49 deletions(-) diff --git a/internal/configs/binance/asset.go b/internal/configs/binance/asset.go index c3a51ff..0d06e06 100644 --- a/internal/configs/binance/asset.go +++ b/internal/configs/binance/asset.go @@ -1,6 +1,9 @@ package binance var SymbolPrefixList = []string{"1000000", "1000", "1M"} + +var SymbolSuffixList = []string{"USDT", "USDC"} + var SymbolSuffixMap = map[string]string{ "USDT": "", "USDC": "c", diff --git a/internal/helper/binancex/resolver.go b/internal/helper/binancex/resolver.go index 817a2be..c60b0b1 100644 --- a/internal/helper/binancex/resolver.go +++ b/internal/helper/binancex/resolver.go @@ -3,11 +3,47 @@ package binancex import ( "strings" + "me.thuanle/bbot/internal/configs/binance" "me.thuanle/bbot/internal/data" + "me.thuanle/bbot/internal/utils/stringx" ) func Token2FutureSymbols(token string) []string { - return Token2Symbols(token) + var syms []string + if !stringx.IsAlphaNumeric(token) { + return syms + } + + token = strings.ToUpper(token) + + for _, prefix := range checkingPrefixList { + prefix = strings.ToUpper(prefix) + + s := prefix + token + + if data.Market.IsFuturesPair(s) { + syms = append(syms, s) + continue + } + + for _, suffix := range binance.SymbolSuffixList { + suffix = strings.ToUpper(suffix) + abbr := strings.ToUpper(binance.SymbolSuffixMap[suffix]) + + sym := s + suffix + if data.Market.IsFuturesPair(sym) { + syms = append(syms, sym) + continue + } + + symAbr, found := stringx.ReplaceSuffix(s, abbr, suffix) + if found && data.Market.IsFuturesPair(symAbr) { + syms = append(syms, symAbr) + continue + } + } + } + return syms } func Token2SpotSymbols(token string) []string { diff --git a/internal/helper/binancex/symbol.go b/internal/helper/binancex/symbol.go index 02287a4..b45c06d 100644 --- a/internal/helper/binancex/symbol.go +++ b/internal/helper/binancex/symbol.go @@ -14,7 +14,8 @@ func Symbol2Token(sym string) string { for _, prefix := range binance.SymbolPrefixList { token, _ = strings.CutPrefix(token, prefix) } - for suffix, abbr := range binance.SymbolSuffixMap { + for _, suffix := range binance.SymbolSuffixList { + abbr := binance.SymbolSuffixMap[suffix] var f bool token, f = stringx.ReplaceSuffix(token, suffix, abbr) if f { @@ -28,57 +29,12 @@ var ( checkingPrefixList = append(binance.SymbolPrefixList, "") ) -func testSym(sym string) bool { - return data.Market.IsFuturesPair(sym) -} - -func Token2Symbols(token string) []string { - var syms []string - if !stringx.IsAlphaNumeric(token) { - return syms - } - - token = strings.ToUpper(token) - - for _, prefix := range checkingPrefixList { - prefix = strings.ToUpper(prefix) - - s := prefix + token - - //no suffix - if testSym(s) { - syms = append(syms, s) - continue - } - - for suffix, abbr := range binance.SymbolSuffixMap { - suffix = strings.ToUpper(suffix) - abbr = strings.ToUpper(abbr) - - //suffix - sym := s + suffix - if testSym(sym) { - syms = append(syms, sym) - continue - } - - //suffix abbr - symAbr, found := stringx.ReplaceSuffix(s, abbr, suffix) - if found && testSym(symAbr) { - syms = append(syms, symAbr) - continue - } - } - } - return syms -} - func IsToken(s string) bool { // First check regular symbols - if len(Token2Symbols(s)) > 0 { + if len(Token2FutureSymbols(s)) > 0 { return true } - + // Then check Alpha tokens s = strings.ToUpper(s) return data.Market.IsAlphaToken(s) -- 2.52.0 From 252e77c000f114410ac991ea2beac602b59d01d8 Mon Sep 17 00:00:00 2001 From: thuanle Date: Mon, 27 Apr 2026 03:46:46 +0700 Subject: [PATCH 2/2] test binance symbol resolver --- internal/helper/binancex/resolver_test.go | 119 +++++++++++++++++++--- 1 file changed, 104 insertions(+), 15 deletions(-) diff --git a/internal/helper/binancex/resolver_test.go b/internal/helper/binancex/resolver_test.go index 5e0767c..b3f13fc 100644 --- a/internal/helper/binancex/resolver_test.go +++ b/internal/helper/binancex/resolver_test.go @@ -1,13 +1,16 @@ package binancex import ( + "reflect" "testing" + "me.thuanle/bbot/internal/configs/binance" "me.thuanle/bbot/internal/data" "me.thuanle/bbot/internal/data/market" ) type resolverMarketStub struct { + alphaTokens map[string]bool spotPairs map[string]bool futuresPairs map[string]bool } @@ -21,7 +24,7 @@ func (m *resolverMarketStub) GetAllPremiumIndex() (map[string]market.PremiumInde 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) IsAlphaToken(symbol string) bool { return m.alphaTokens[symbol] } func (m *resolverMarketStub) GetAlphaToken(symbol string) (market.AlphaTokenInfo, bool) { return market.AlphaTokenInfo{}, false } @@ -30,16 +33,20 @@ func (m *resolverMarketStub) IsSpotPair(symbol string) bool { retu func (m *resolverMarketStub) IsFuturesPair(symbol string) bool { return m.futuresPairs[symbol] } func (m *resolverMarketStub) RefreshTradingPairCache() error { return nil } -func TestToken2FutureSymbols_ResolvesPrefixAndSuffix(t *testing.T) { +func withResolverMarketStub(t *testing.T, marketStub *resolverMarketStub) { + t.Helper() orig := data.Market - defer func() { data.Market = orig }() + data.Market = marketStub + t.Cleanup(func() { data.Market = orig }) +} - data.Market = &resolverMarketStub{ +func TestToken2FutureSymbols_ResolvesPrefixAndSuffix(t *testing.T) { + withResolverMarketStub(t, &resolverMarketStub{ futuresPairs: map[string]bool{ "1000PEPEUSDT": true, "1000PEPEUSDC": true, }, - } + }) syms := Token2FutureSymbols("pepe") if len(syms) == 0 { @@ -62,14 +69,99 @@ func TestToken2FutureSymbols_ResolvesPrefixAndSuffix(t *testing.T) { } } -func TestToken2SpotSymbols_AppliesExplicitRemap(t *testing.T) { - orig := data.Market - defer func() { data.Market = orig }() +func TestToken2FutureSymbols_UsesDeterministicCandidateOrder(t *testing.T) { + withResolverMarketStub(t, &resolverMarketStub{ + futuresPairs: map[string]bool{ + "1000PEPEUSDT": true, + "1000PEPEUSDC": true, + "PEPEUSDT": true, + "PEPEUSDC": true, + }, + }) - data.Market = &resolverMarketStub{ + syms := Token2FutureSymbols("pepe") + expected := []string{"1000PEPEUSDT", "1000PEPEUSDC", "PEPEUSDT", "PEPEUSDC"} + if !reflect.DeepEqual(syms, expected) { + t.Fatalf("expected deterministic order %+v, got %+v", expected, syms) + } +} + +func TestToken2FutureSymbols_ResolvesUSDCAbbreviation(t *testing.T) { + withResolverMarketStub(t, &resolverMarketStub{ + futuresPairs: map[string]bool{"PEPEUSDC": true}, + }) + + syms := Token2FutureSymbols("pepec") + expected := []string{"PEPEUSDC"} + if !reflect.DeepEqual(syms, expected) { + t.Fatalf("expected USDC abbreviation resolution %+v, got %+v", expected, syms) + } +} + +func TestSymbolSuffixListMatchesSymbolSuffixMap(t *testing.T) { + seen := make(map[string]struct{}, len(binance.SymbolSuffixList)) + for _, suffix := range binance.SymbolSuffixList { + if _, ok := binance.SymbolSuffixMap[suffix]; !ok { + t.Fatalf("SymbolSuffixList contains %q missing from SymbolSuffixMap", suffix) + } + seen[suffix] = struct{}{} + } + + for suffix := range binance.SymbolSuffixMap { + if _, ok := seen[suffix]; !ok { + t.Fatalf("SymbolSuffixMap contains %q missing from SymbolSuffixList", suffix) + } + } +} + +func TestIsToken(t *testing.T) { + tests := []struct { + name string + input string + marketStub *resolverMarketStub + want bool + }{ + { + name: "futures token resolution success", + input: "pepe", + marketStub: &resolverMarketStub{ + futuresPairs: map[string]bool{"1000PEPEUSDT": true}, + }, + want: true, + }, + { + name: "alpha token fallback", + input: "alpha", + marketStub: &resolverMarketStub{ + alphaTokens: map[string]bool{"ALPHA": true}, + }, + want: true, + }, + { + name: "non alphanumeric input", + input: "bad!", + marketStub: &resolverMarketStub{}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + withResolverMarketStub(t, tt.marketStub) + + got := IsToken(tt.input) + if got != tt.want { + t.Fatalf("expected %v, got %v", tt.want, got) + } + }) + } +} + +func TestToken2SpotSymbols_AppliesExplicitRemap(t *testing.T) { + withResolverMarketStub(t, &resolverMarketStub{ futuresPairs: map[string]bool{"LUNA2USDT": true}, spotPairs: map[string]bool{"LUNAUSDT": true}, - } + }) spots := Token2SpotSymbols("luna2") if len(spots) != 1 || spots[0] != "LUNAUSDT" { @@ -78,12 +170,9 @@ func TestToken2SpotSymbols_AppliesExplicitRemap(t *testing.T) { } func TestToken2SpotSymbols_SpotOnlyFallback(t *testing.T) { - orig := data.Market - defer func() { data.Market = orig }() - - data.Market = &resolverMarketStub{ + withResolverMarketStub(t, &resolverMarketStub{ spotPairs: map[string]bool{"ABCUSDT": true}, - } + }) spots := Token2SpotSymbols("abc") if len(spots) != 1 || spots[0] != "ABCUSDT" { -- 2.52.0