fix: restore canonical token lookup for market caches
Build Docker Image / build (amd64) (push) Successful in 1m25s

This commit is contained in:
2026-06-08 07:17:14 +07:00
parent 3b726311f5
commit e173fd7799
5 changed files with 150 additions and 23 deletions
+2 -7
View File
@@ -25,17 +25,16 @@ func (ms *MarketData) refreshFuturePairCache() error {
continue continue
} }
futurePairs[s.Symbol] = true futurePairs[s.Symbol] = true
token := parseTokenFromSymbolByQuotePriority(s.Symbol) token := tokenKeyFromSymbol(s.Symbol)
if token == "" { if token == "" {
continue continue
} }
token = futureCacheTokenKey(token)
futureTokenCandidates[token] = append(futureTokenCandidates[token], s.Symbol) futureTokenCandidates[token] = append(futureTokenCandidates[token], s.Symbol)
} }
futureToken2Symbol := make(map[string]string, len(futureTokenCandidates)) futureToken2Symbol := make(map[string]string, len(futureTokenCandidates))
for token, candidates := range futureTokenCandidates { for token, candidates := range futureTokenCandidates {
futureToken2Symbol[token] = selectCanonicalSymbolByQuotePriority(token, candidates) futureToken2Symbol[token] = selectCanonicalFutureSymbol(token, candidates)
} }
ms.pairCacheMutex.Lock() ms.pairCacheMutex.Lock()
@@ -47,10 +46,6 @@ func (ms *MarketData) refreshFuturePairCache() error {
return nil return nil
} }
func futureCacheTokenKey(token string) string {
return strings.ToUpper(token)
}
func (ms *MarketData) IsFuturesPair(symbol string) bool { func (ms *MarketData) IsFuturesPair(symbol string) bool {
ms.pairCacheMutex.RLock() ms.pairCacheMutex.RLock()
defer ms.pairCacheMutex.RUnlock() defer ms.pairCacheMutex.RUnlock()
+27 -4
View File
@@ -26,16 +26,39 @@ func TestSelectCanonicalSymbolByQuotePriority_NoPreferredQuote(t *testing.T) {
} }
} }
func TestFutureCacheTokenKey_PreservesRawFutureToken(t *testing.T) { func TestSelectCanonicalFutureSymbol_PrefersPrefixThenQuote(t *testing.T) {
got := futureCacheTokenKey("LUNA2") 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" { if got != "LUNA2" {
t.Fatalf("expected LUNA2, got %q", got) t.Fatalf("expected LUNA2, got %q", got)
} }
} }
func TestFutureCacheTokenKey_NoOverride(t *testing.T) { func TestTokenKeyFromSymbol_StripsKnownPrefix(t *testing.T) {
got := futureCacheTokenKey("PEPE") got := tokenKeyFromSymbol("1000PEPEUSDT")
if got != "PEPE" { if got != "PEPE" {
t.Fatalf("expected PEPE, got %q", got) 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)
}
}
+2 -2
View File
@@ -27,7 +27,7 @@ func (ms *MarketData) refreshSpotPairCache() error {
continue continue
} }
spotPairs[s.Symbol] = true spotPairs[s.Symbol] = true
token := parseTokenFromSymbolByQuotePriority(s.Symbol) token := tokenKeyFromSymbol(s.Symbol)
if token == "" { if token == "" {
continue continue
} }
@@ -36,7 +36,7 @@ func (ms *MarketData) refreshSpotPairCache() error {
spotToken2Symbol := make(map[string]string, len(spotTokenCandidates)) spotToken2Symbol := make(map[string]string, len(spotTokenCandidates))
for token, candidates := range spotTokenCandidates { for token, candidates := range spotTokenCandidates {
spotToken2Symbol[token] = selectCanonicalSymbolByQuotePriority(token, candidates) spotToken2Symbol[token] = selectCanonicalSpotSymbol(token, candidates)
} }
ms.pairCacheMutex.Lock() ms.pairCacheMutex.Lock()
+78
View File
@@ -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]
}
+40 -9
View File
@@ -1,12 +1,14 @@
package binancex package binancex
import ( import (
"sort"
"strings" "strings"
"testing" "testing"
"me.thuanle/bbot/internal/configs/binance" "me.thuanle/bbot/internal/configs/binance"
"me.thuanle/bbot/internal/data" "me.thuanle/bbot/internal/data"
"me.thuanle/bbot/internal/data/market" "me.thuanle/bbot/internal/data/market"
"me.thuanle/bbot/internal/utils/stringx"
) )
type resolverMarketStub struct { type resolverMarketStub struct {
@@ -32,23 +34,52 @@ func (m *resolverMarketStub) GetAlphaPrice(symbol string) (float64, bool) { retu
func (m *resolverMarketStub) IsSpotPair(symbol string) bool { return m.spotPairs[symbol] } 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) IsFuturesPair(symbol string) bool { return m.futuresPairs[symbol] }
func (m *resolverMarketStub) GetSpotSymbolByToken(token string) (string, bool) { func (m *resolverMarketStub) GetSpotSymbolByToken(token string) (string, bool) {
token = strings.ToUpper(token) return selectCanonicalTestSymbol(token, m.spotPairs, []string{""})
for sym := range m.spotPairs {
if strings.ToUpper(Symbol2Token(sym)) == token {
return sym, true
}
}
return "", false
} }
func (m *resolverMarketStub) GetFutureSymbolByToken(token string) (string, bool) { 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) 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 { if strings.ToUpper(Symbol2Token(sym)) == token {
return sym, true sym = strings.ToUpper(sym)
candidates = append(candidates, sym)
candidateSet[sym] = struct{}{}
} }
} }
if len(candidates) == 0 {
return "", false 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 } func (m *resolverMarketStub) RefreshTradingPairCache() error { return nil }
func withResolverMarketStub(t *testing.T, marketStub *resolverMarketStub) { func withResolverMarketStub(t *testing.T, marketStub *resolverMarketStub) {