package market import ( "context" "sort" "strings" "time" "github.com/rs/zerolog/log" "me.thuanle/bbot/internal/configs/binance" ) func (ms *MarketData) refreshSpotPairCache() error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() spotInfo, err := ms.spotClient.NewExchangeInfoService().Do(ctx) if err != nil { log.Error().Err(err).Msg("Failed to fetch spot exchange info") return err } spotPairs := make(map[string]bool, len(spotInfo.Symbols)) spotTokenCandidates := make(map[string][]string) for _, s := range spotInfo.Symbols { if s.Status != "TRADING" { continue } spotPairs[s.Symbol] = true token := parseTokenFromSymbolByQuotePriority(s.Symbol) if token == "" { continue } spotTokenCandidates[token] = append(spotTokenCandidates[token], s.Symbol) } spotToken2Symbol := make(map[string]string, len(spotTokenCandidates)) for token, candidates := range spotTokenCandidates { spotToken2Symbol[token] = selectCanonicalSymbolByQuotePriority(token, candidates) } ms.pairCacheMutex.Lock() ms.spotPairs = spotPairs ms.spotToken2Symbol = spotToken2Symbol ms.lastPairCacheUpdate = time.Now() ms.pairCacheMutex.Unlock() return nil } func (ms *MarketData) refreshFuturePairCache() error { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() futuresInfo, err := ms.futuresClient.NewExchangeInfoService().Do(ctx) if err != nil { log.Error().Err(err).Msg("Failed to fetch futures exchange info") return err } futurePairs := make(map[string]bool, len(futuresInfo.Symbols)) futureTokenCandidates := make(map[string][]string) for _, s := range futuresInfo.Symbols { if s.Status != "TRADING" { continue } futurePairs[s.Symbol] = true token := parseTokenFromSymbolByQuotePriority(s.Symbol) if token == "" { continue } token = normalizeFutureToken(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) } ms.pairCacheMutex.Lock() ms.futuresPairs = futurePairs ms.futureToken2Symbol = futureToken2Symbol ms.lastPairCacheUpdate = time.Now() ms.pairCacheMutex.Unlock() return nil } func (ms *MarketData) refreshTradingPairCache() error { if err := ms.refreshSpotPairCache(); err != nil { return err } if err := ms.refreshFuturePairCache(); err != nil { return err } ms.pairCacheMutex.RLock() spotCount := len(ms.spotPairs) futureCount := len(ms.futuresPairs) ms.pairCacheMutex.RUnlock() log.Info(). Int("spot", spotCount). Int("futures", futureCount). Msg("Trading pair cache refreshed") return nil } func (ms *MarketData) cacheRefreshLoop() { ms.refreshAllCaches() ticker := time.NewTicker(time.Hour) defer ticker.Stop() for range ticker.C { ms.refreshAllCaches() } } func (ms *MarketData) refreshAllCaches() { if err := ms.refreshSpotPairCache(); err != nil { log.Error().Err(err).Msg("Failed spot pair refresh") } if err := ms.refreshFuturePairCache(); err != nil { log.Error().Err(err).Msg("Failed futures pair refresh") } ms.refreshAlphaTokenCache() } func parseTokenFromSymbolByQuotePriority(symbol string) string { symbol = strings.ToUpper(symbol) for _, quote := range binance.QuotePriority { quote = strings.ToUpper(quote) if strings.HasSuffix(symbol, quote) { token := strings.TrimSuffix(symbol, quote) if token != "" { return token } } } return "" } func selectCanonicalSymbolByQuotePriority(token string, candidates []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)) for _, c := range candidates { normalized = append(normalized, strings.ToUpper(c)) } for _, quote := range binance.QuotePriority { target := token + strings.ToUpper(quote) for _, c := range normalized { if c == target { return c } } } sort.Strings(normalized) return normalized[0] } func normalizeFutureToken(token string) string { token = strings.ToUpper(token) if mapped, ok := binance.FutureToken2SpotTokenMap[token]; ok { return strings.ToUpper(mapped) } return token } func (ms *MarketData) IsSpotPair(symbol string) bool { ms.pairCacheMutex.RLock() defer ms.pairCacheMutex.RUnlock() return ms.spotPairs[symbol] } func (ms *MarketData) IsFuturesPair(symbol string) bool { ms.pairCacheMutex.RLock() defer ms.pairCacheMutex.RUnlock() return ms.futuresPairs[symbol] } func (ms *MarketData) GetSpotSymbolByToken(token string) (string, bool) { ms.pairCacheMutex.RLock() defer ms.pairCacheMutex.RUnlock() sym, ok := ms.spotToken2Symbol[strings.ToUpper(token)] return sym, ok } func (ms *MarketData) GetFutureSymbolByToken(token string) (string, bool) { ms.pairCacheMutex.RLock() defer ms.pairCacheMutex.RUnlock() sym, ok := ms.futureToken2Symbol[strings.ToUpper(token)] return sym, ok } func (ms *MarketData) RefreshTradingPairCache() error { return ms.refreshTradingPairCache() }