From 6e8a2e8f6853e044ac3b6b8cbc217a7e17f59d18 Mon Sep 17 00:00:00 2001 From: thuanle Date: Sun, 26 Apr 2026 18:13:13 +0700 Subject: [PATCH] fix: decouple token spot/future/alpha lookups Collect token market data independently for spot, futures, and alpha so any available source can be rendered even if others fail. Co-Authored-By: Claude Opus 4.7 --- internal/services/tele/commands/token.go | 82 +++++++---------- internal/services/tele/commands/token_test.go | 88 ++++++++++++++++++- 2 files changed, 119 insertions(+), 51 deletions(-) diff --git a/internal/services/tele/commands/token.go b/internal/services/tele/commands/token.go index 01ce105..fca9e41 100644 --- a/internal/services/tele/commands/token.go +++ b/internal/services/tele/commands/token.go @@ -6,7 +6,6 @@ import ( "gopkg.in/telebot.v3" "me.thuanle/bbot/internal/configs/tele" "me.thuanle/bbot/internal/data" - "me.thuanle/bbot/internal/helper/binancex" "me.thuanle/bbot/internal/services/tele/chat" "me.thuanle/bbot/internal/services/tele/view" ) @@ -62,68 +61,51 @@ func showStickerMode(context telebot.Context, token string) { } } -func OnTokenInfoByToken(context telebot.Context, token string) error { - token = strings.ToUpper(token) - showStickerMode(context, token) - - var ( - hasAlpha, hasAlpha24h bool - alphaPrice, alpha24h float64 - hasFuture, hasSpot bool - futurePrice float64 - fundingRate float64 - fundingTime int64 - spotPrice float64 - ) +func collectRichTokenData(token string) buildRichTokenMessageArgs { + a := buildRichTokenMessageArgs{Token: token} if alphaToken, ok := data.Market.GetAlphaToken(token); ok { - hasAlpha = true - alphaPrice = alphaToken.GetPrice() - hasAlpha24h = true - alpha24h = alphaToken.GetPercentChange24h() + a.HasAlpha = true + a.AlphaPrice = alphaToken.GetPrice() + a.HasAlpha24h = true + a.Alpha24h = alphaToken.GetPercentChange24h() } - symbols := binancex.Token2Symbols(token) - if len(symbols) > 0 { - fp, fr, ft, ok := data.Market.GetFuturePrice(symbols[0]) - if ok { - hasFuture = true - futurePrice = fp - fundingRate = fr - fundingTime = ft + futureSymbol := token + "USDT" + if data.Market.IsFuturesPair(futureSymbol) { + if fp, fr, ft, ok := data.Market.GetFuturePrice(futureSymbol); ok { + a.HasFuture = true + a.FuturePrice = fp + a.FundingRate = fr + a.FundingTimeMs = ft + } + } - sSymbol := binancex.Future2SpotSymbol(symbols[0]) - if sp, ok := data.Market.GetSpotPrice(sSymbol); ok { - hasSpot = true - spotPrice = sp - } + spotSymbol := token + "USDT" + if data.Market.IsSpotPair(spotSymbol) { + if sp, ok := data.Market.GetSpotPrice(spotSymbol); ok { + a.HasSpot = true + a.SpotPrice = sp } } marginRates := data.Market.GetMarginInterestRates() - marginAPR := marginRates[token] * 365 * 100 - hasMarginAPR := marginRates[token] != 0 + a.MarginAPRPercent = marginRates[token] * 365 * 100 + a.HasMarginAPR = marginRates[token] != 0 - if !hasSpot && !hasFuture && !hasAlpha { + return a +} + +func OnTokenInfoByToken(context telebot.Context, token string) error { + token = strings.ToUpper(token) + showStickerMode(context, token) + + args := collectRichTokenData(token) + if !args.HasSpot && !args.HasFuture && !args.HasAlpha { return nil } - msg := view.RenderRichTokenMessage(buildRichTokenMessageInput(buildRichTokenMessageArgs{ - Token: token, - HasSpot: hasSpot, - SpotPrice: spotPrice, - HasFuture: hasFuture, - FuturePrice: futurePrice, - FundingRate: fundingRate, - FundingTimeMs: fundingTime, - HasAlpha: hasAlpha, - AlphaPrice: alphaPrice, - HasAlpha24h: hasAlpha24h, - Alpha24h: alpha24h, - HasMarginAPR: hasMarginAPR, - MarginAPRPercent: marginAPR, - })) - + msg := view.RenderRichTokenMessage(buildRichTokenMessageInput(args)) _ = chat.ReplyMessage(context, msg) return nil } diff --git a/internal/services/tele/commands/token_test.go b/internal/services/tele/commands/token_test.go index 21bbb99..0356891 100644 --- a/internal/services/tele/commands/token_test.go +++ b/internal/services/tele/commands/token_test.go @@ -1,6 +1,11 @@ package commands -import "testing" +import ( + "testing" + + "me.thuanle/bbot/internal/data" + "me.thuanle/bbot/internal/data/market" +) func TestBuildRichTokenMessageInput_AllSources(t *testing.T) { in := buildRichTokenMessageInput(buildRichTokenMessageArgs{ @@ -40,3 +45,84 @@ func TestBuildRichTokenMessageInput_OnlyAlpha(t *testing.T) { t.Fatalf("did not expect spot/future flags true: %+v", in) } } + +type marketStub struct { + spotPairs map[string]bool + futuresPairs map[string]bool + spotPrices map[string]float64 + futurePrices map[string]struct { + price float64 + rate float64 + time int64 + } + alphaTokens map[string]market.AlphaTokenInfo + marginRates map[string]float64 +} + +func (m *marketStub) GetFuturePrice(symbol string) (float64, float64, int64, bool) { + v, ok := m.futurePrices[symbol] + if !ok { + return 0, 0, 0, false + } + return v.price, v.rate, v.time, true +} +func (m *marketStub) GetAllPremiumIndex() (map[string]market.PremiumIndex, error) { return nil, nil } +func (m *marketStub) GetAllFundRate() (map[string]float64, map[string]int64) { return nil, nil } +func (m *marketStub) GetSpotPrice(symbol string) (float64, bool) { + v, ok := m.spotPrices[symbol] + return v, ok +} +func (m *marketStub) GetMarginInterestRates() map[string]float64 { return m.marginRates } +func (m *marketStub) IsAlphaToken(symbol string) bool { + _, ok := m.alphaTokens[symbol] + return ok +} +func (m *marketStub) GetAlphaToken(symbol string) (market.AlphaTokenInfo, bool) { + v, ok := m.alphaTokens[symbol] + return v, ok +} +func (m *marketStub) IsSpotPair(symbol string) bool { return m.spotPairs[symbol] } +func (m *marketStub) IsFuturesPair(symbol string) bool { return m.futuresPairs[symbol] } +func (m *marketStub) RefreshTradingPairCache() error { return nil } + +func TestCollectRichTokenData_SpotOnlyReachable(t *testing.T) { + orig := data.Market + defer func() { data.Market = orig }() + + data.Market = &marketStub{ + spotPairs: map[string]bool{"ABCUSDT": true}, + spotPrices: map[string]float64{"ABCUSDT": 1.23}, + } + + args := collectRichTokenData("ABC") + if !args.HasSpot { + t.Fatalf("expected spot to be reachable for spot-only token: %+v", args) + } + if args.HasFuture || args.HasAlpha { + t.Fatalf("did not expect future/alpha for spot-only token: %+v", args) + } +} + +func TestCollectRichTokenData_FutureFailureStillKeepsSpot(t *testing.T) { + orig := data.Market + defer func() { data.Market = orig }() + + data.Market = &marketStub{ + spotPairs: map[string]bool{"ETHUSDT": true}, + futuresPairs: map[string]bool{"ETHUSDT": true}, + spotPrices: map[string]float64{"ETHUSDT": 3245}, + futurePrices: map[string]struct { + price float64 + rate float64 + time int64 + }{}, + } + + args := collectRichTokenData("ETH") + if !args.HasSpot { + t.Fatalf("expected spot to remain available when future lookup fails: %+v", args) + } + if args.HasFuture { + t.Fatalf("did not expect future when future lookup fails: %+v", args) + } +}