diff --git a/internal/data/market/futures_pairs.go b/internal/data/market/futures_pairs.go new file mode 100644 index 0000000..d45ac02 --- /dev/null +++ b/internal/data/market/futures_pairs.go @@ -0,0 +1,65 @@ +package market + +import ( + "context" + "strings" + "time" + + "github.com/rs/zerolog/log" +) + +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 = 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) + } + + ms.pairCacheMutex.Lock() + ms.futuresPairs = futurePairs + ms.futureToken2Symbol = futureToken2Symbol + ms.lastPairCacheUpdate = time.Now() + ms.pairCacheMutex.Unlock() + + 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() + return ms.futuresPairs[symbol] +} + +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 +} diff --git a/internal/data/market/future_price.go b/internal/data/market/futures_prices.go similarity index 100% rename from internal/data/market/future_price.go rename to internal/data/market/futures_prices.go diff --git a/internal/data/market/main.go b/internal/data/market/main.go index 8acf819..e69abe3 100644 --- a/internal/data/market/main.go +++ b/internal/data/market/main.go @@ -54,3 +54,47 @@ func (ms *MarketData) alphaCacheRefreshLoop() { ms.refreshAlphaTokenCache() } } + +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 (ms *MarketData) RefreshTradingPairCache() error { + return ms.refreshTradingPairCache() +} diff --git a/internal/data/market/spot_pairs.go b/internal/data/market/spot_pairs.go new file mode 100644 index 0000000..43fdd82 --- /dev/null +++ b/internal/data/market/spot_pairs.go @@ -0,0 +1,103 @@ +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 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 (ms *MarketData) IsSpotPair(symbol string) bool { + ms.pairCacheMutex.RLock() + defer ms.pairCacheMutex.RUnlock() + return ms.spotPairs[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 +} diff --git a/internal/data/market/spot_price.go b/internal/data/market/spot_prices.go similarity index 100% rename from internal/data/market/spot_price.go rename to internal/data/market/spot_prices.go diff --git a/internal/data/market/trading_pairs.go b/internal/data/market/trading_pairs.go deleted file mode 100644 index e19e31e..0000000 --- a/internal/data/market/trading_pairs.go +++ /dev/null @@ -1,203 +0,0 @@ -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 = 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) - } - - 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 futureCacheTokenKey(token string) string { - return strings.ToUpper(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() -}