From e173fd7799acc73fb31bc348bae92583b75aeae0 Mon Sep 17 00:00:00 2001 From: codex Date: Mon, 8 Jun 2026 07:17:14 +0700 Subject: [PATCH] fix: restore canonical token lookup for market caches --- internal/data/market/futures_pairs.go | 9 +-- internal/data/market/pairs_test.go | 31 +++++++-- internal/data/market/spot_pairs.go | 4 +- internal/data/market/token_lookup.go | 78 +++++++++++++++++++++++ internal/helper/binancex/resolver_test.go | 51 ++++++++++++--- 5 files changed, 150 insertions(+), 23 deletions(-) create mode 100644 internal/data/market/token_lookup.go diff --git a/internal/data/market/futures_pairs.go b/internal/data/market/futures_pairs.go index d45ac02..072e859 100644 --- a/internal/data/market/futures_pairs.go +++ b/internal/data/market/futures_pairs.go @@ -25,17 +25,16 @@ func (ms *MarketData) refreshFuturePairCache() error { continue } futurePairs[s.Symbol] = true - token := parseTokenFromSymbolByQuotePriority(s.Symbol) + token := tokenKeyFromSymbol(s.Symbol) if token == "" { continue } - token = futureCacheTokenKey(token) futureTokenCandidates[token] = append(futureTokenCandidates[token], s.Symbol) } futureToken2Symbol := make(map[string]string, len(futureTokenCandidates)) for token, candidates := range futureTokenCandidates { - futureToken2Symbol[token] = selectCanonicalSymbolByQuotePriority(token, candidates) + futureToken2Symbol[token] = selectCanonicalFutureSymbol(token, candidates) } ms.pairCacheMutex.Lock() @@ -47,10 +46,6 @@ func (ms *MarketData) refreshFuturePairCache() error { return nil } -func futureCacheTokenKey(token string) string { - return strings.ToUpper(token) -} - func (ms *MarketData) IsFuturesPair(symbol string) bool { ms.pairCacheMutex.RLock() defer ms.pairCacheMutex.RUnlock() diff --git a/internal/data/market/pairs_test.go b/internal/data/market/pairs_test.go index 0e7c94f..06177b1 100644 --- a/internal/data/market/pairs_test.go +++ b/internal/data/market/pairs_test.go @@ -26,16 +26,39 @@ func TestSelectCanonicalSymbolByQuotePriority_NoPreferredQuote(t *testing.T) { } } -func TestFutureCacheTokenKey_PreservesRawFutureToken(t *testing.T) { - got := futureCacheTokenKey("LUNA2") +func TestSelectCanonicalFutureSymbol_PrefersPrefixThenQuote(t *testing.T) { + pairs := []string{"PEPEUSDC", "PEPEUSDT", "1000PEPEUSDC", "1000PEPEUSDT"} + got := selectCanonicalFutureSymbol("PEPE", pairs) + if got != "1000PEPEUSDT" { + t.Fatalf("expected 1000PEPEUSDT, got %q", got) + } +} + +func TestSelectCanonicalFutureSymbol_UsesQuoteAbbreviation(t *testing.T) { + pairs := []string{"PEPEUSDC"} + got := selectCanonicalFutureSymbol("PEPEC", pairs) + if got != "PEPEUSDC" { + t.Fatalf("expected PEPEUSDC, got %q", got) + } +} + +func TestTokenKeyFromSymbol_PreservesUnmappedFutureToken(t *testing.T) { + got := tokenKeyFromSymbol("LUNA2USDT") if got != "LUNA2" { t.Fatalf("expected LUNA2, got %q", got) } } -func TestFutureCacheTokenKey_NoOverride(t *testing.T) { - got := futureCacheTokenKey("PEPE") +func TestTokenKeyFromSymbol_StripsKnownPrefix(t *testing.T) { + got := tokenKeyFromSymbol("1000PEPEUSDT") if got != "PEPE" { t.Fatalf("expected PEPE, got %q", got) } } + +func TestTokenKeyFromSymbol_UsesQuoteAbbreviation(t *testing.T) { + got := tokenKeyFromSymbol("PEPEUSDC") + if got != "PEPEC" { + t.Fatalf("expected PEPEC, got %q", got) + } +} diff --git a/internal/data/market/spot_pairs.go b/internal/data/market/spot_pairs.go index 43fdd82..c3ca86e 100644 --- a/internal/data/market/spot_pairs.go +++ b/internal/data/market/spot_pairs.go @@ -27,7 +27,7 @@ func (ms *MarketData) refreshSpotPairCache() error { continue } spotPairs[s.Symbol] = true - token := parseTokenFromSymbolByQuotePriority(s.Symbol) + token := tokenKeyFromSymbol(s.Symbol) if token == "" { continue } @@ -36,7 +36,7 @@ func (ms *MarketData) refreshSpotPairCache() error { spotToken2Symbol := make(map[string]string, len(spotTokenCandidates)) for token, candidates := range spotTokenCandidates { - spotToken2Symbol[token] = selectCanonicalSymbolByQuotePriority(token, candidates) + spotToken2Symbol[token] = selectCanonicalSpotSymbol(token, candidates) } ms.pairCacheMutex.Lock() diff --git a/internal/data/market/token_lookup.go b/internal/data/market/token_lookup.go new file mode 100644 index 0000000..28e9d94 --- /dev/null +++ b/internal/data/market/token_lookup.go @@ -0,0 +1,78 @@ +package market + +import ( + "sort" + "strings" + + "me.thuanle/bbot/internal/configs/binance" + "me.thuanle/bbot/internal/utils/stringx" +) + +var futureSymbolPrefixPriority = append(append([]string{}, binance.SymbolPrefixList...), "") + +func tokenKeyFromSymbol(symbol string) string { + token := strings.ToUpper(symbol) + + for _, prefix := range binance.SymbolPrefixList { + token, _ = strings.CutPrefix(token, strings.ToUpper(prefix)) + } + + for _, suffix := range binance.SymbolSuffixList { + replaced, ok := stringx.ReplaceSuffix(token, strings.ToUpper(suffix), strings.ToUpper(binance.SymbolSuffixMap[suffix])) + if ok { + return replaced + } + } + + return "" +} + +func selectCanonicalSpotSymbol(token string, candidates []string) string { + return selectCanonicalSymbolByTokenOrder(token, candidates, []string{""}) +} + +func selectCanonicalFutureSymbol(token string, candidates []string) string { + return selectCanonicalSymbolByTokenOrder(token, candidates, futureSymbolPrefixPriority) +} + +func selectCanonicalSymbolByTokenOrder(token string, candidates []string, prefixes []string) string { + if len(candidates) == 0 { + return "" + } + if len(candidates) == 1 { + return strings.ToUpper(candidates[0]) + } + + token = strings.ToUpper(token) + normalized := make([]string, 0, len(candidates)) + candidateSet := make(map[string]struct{}, len(candidates)) + for _, candidate := range candidates { + candidate = strings.ToUpper(candidate) + normalized = append(normalized, candidate) + candidateSet[candidate] = struct{}{} + } + + for _, prefix := range prefixes { + base := strings.ToUpper(prefix) + token + if _, ok := candidateSet[base]; ok { + return base + } + + for _, suffix := range binance.SymbolSuffixList { + target := base + strings.ToUpper(suffix) + if _, ok := candidateSet[target]; ok { + return target + } + + replaced, ok := stringx.ReplaceSuffix(base, strings.ToUpper(binance.SymbolSuffixMap[suffix]), strings.ToUpper(suffix)) + if ok { + if _, exists := candidateSet[replaced]; exists { + return replaced + } + } + } + } + + sort.Strings(normalized) + return normalized[0] +} diff --git a/internal/helper/binancex/resolver_test.go b/internal/helper/binancex/resolver_test.go index 25e8fdf..57d596e 100644 --- a/internal/helper/binancex/resolver_test.go +++ b/internal/helper/binancex/resolver_test.go @@ -1,12 +1,14 @@ package binancex import ( + "sort" "strings" "testing" "me.thuanle/bbot/internal/configs/binance" "me.thuanle/bbot/internal/data" "me.thuanle/bbot/internal/data/market" + "me.thuanle/bbot/internal/utils/stringx" ) type resolverMarketStub struct { @@ -32,22 +34,51 @@ func (m *resolverMarketStub) GetAlphaPrice(symbol string) (float64, bool) { retu 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) GetSpotSymbolByToken(token string) (string, bool) { - token = strings.ToUpper(token) - for sym := range m.spotPairs { - if strings.ToUpper(Symbol2Token(sym)) == token { - return sym, true - } - } - return "", false + return selectCanonicalTestSymbol(token, m.spotPairs, []string{""}) } func (m *resolverMarketStub) GetFutureSymbolByToken(token string) (string, bool) { + prefixes := append(append([]string{}, binance.SymbolPrefixList...), "") + return selectCanonicalTestSymbol(token, m.futuresPairs, prefixes) +} + +func selectCanonicalTestSymbol(token string, pairs map[string]bool, prefixes []string) (string, bool) { token = strings.ToUpper(token) - for sym := range m.futuresPairs { + candidates := make([]string, 0, len(pairs)) + candidateSet := make(map[string]struct{}, len(pairs)) + for sym := range pairs { if strings.ToUpper(Symbol2Token(sym)) == token { - return sym, true + sym = strings.ToUpper(sym) + candidates = append(candidates, sym) + candidateSet[sym] = struct{}{} } } - return "", false + if len(candidates) == 0 { + return "", false + } + + for _, prefix := range prefixes { + base := strings.ToUpper(prefix) + token + if _, ok := candidateSet[base]; ok { + return base, true + } + + for _, quote := range binance.SymbolSuffixList { + target := base + strings.ToUpper(quote) + if _, ok := candidateSet[target]; ok { + return target, true + } + + replaced, ok := stringx.ReplaceSuffix(base, strings.ToUpper(binance.SymbolSuffixMap[quote]), strings.ToUpper(quote)) + if ok { + if _, exists := candidateSet[replaced]; exists { + return replaced, true + } + } + } + } + + sort.Strings(candidates) + return candidates[0], true } func (m *resolverMarketStub) RefreshTradingPairCache() error { return nil }