From 6e8a2e8f6853e044ac3b6b8cbc217a7e17f59d18 Mon Sep 17 00:00:00 2001 From: thuanle Date: Sun, 26 Apr 2026 18:13:13 +0700 Subject: [PATCH 1/2] 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) + } +} -- 2.52.0 From 82b0f2f1a8007daa6d49c97c868d967eec09e28a Mon Sep 17 00:00:00 2001 From: thuanle Date: Sun, 26 Apr 2026 18:22:48 +0700 Subject: [PATCH 2/2] feat: fetch alpha price by symbol ticker Keep alpha token existence from list cache and fetch live alpha price via symbol ticker endpoint for richer token response accuracy. Co-Authored-By: Claude Opus 4.7 --- internal/data/imarket.go | 1 + internal/data/market/alpha_tokens.go | 52 +++++++++++++++++-- internal/services/tele/commands/token.go | 6 ++- internal/services/tele/commands/token_test.go | 28 ++++++++++ 4 files changed, 83 insertions(+), 4 deletions(-) diff --git a/internal/data/imarket.go b/internal/data/imarket.go index a7d8a21..1582529 100644 --- a/internal/data/imarket.go +++ b/internal/data/imarket.go @@ -13,6 +13,7 @@ type IMarket interface { // Alpha token methods IsAlphaToken(symbol string) bool GetAlphaToken(symbol string) (market.AlphaTokenInfo, bool) + GetAlphaPrice(symbol string) (float64, bool) // Trading pair methods IsSpotPair(symbol string) bool diff --git a/internal/data/market/alpha_tokens.go b/internal/data/market/alpha_tokens.go index 4676ff2..4e99972 100644 --- a/internal/data/market/alpha_tokens.go +++ b/internal/data/market/alpha_tokens.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "strconv" "time" @@ -54,10 +55,21 @@ type AlphaTokenInfo struct { // AlphaTokenResponse represents the API response structure type AlphaTokenResponse struct { - Code string `json:"code"` - Message *string `json:"message"` + Code string `json:"code"` + Message *string `json:"message"` + MessageDetail *string `json:"messageDetail"` + Data []AlphaTokenInfo `json:"data"` +} + +type AlphaTickerData struct { + Price string `json:"price"` +} + +type AlphaTickerResponse struct { + Code string `json:"code"` + Message *string `json:"message"` MessageDetail *string `json:"messageDetail"` - Data []AlphaTokenInfo `json:"data"` + Data AlphaTickerData `json:"data"` } // GetPrice returns the price as float64 @@ -150,3 +162,37 @@ func (ms *MarketData) IsAlphaToken(symbol string) bool { _, exists := ms.GetAlphaToken(symbol) return exists } + +func (ms *MarketData) GetAlphaPrice(symbol string) (float64, bool) { + const alphaTickerURL = "https://www.binance.com/bapi/defi/v1/public/alpha-trade/ticker" + + client := &http.Client{Timeout: 5 * time.Second} + endpoint := alphaTickerURL + "?" + url.Values{"symbol": []string{symbol}}.Encode() + + resp, err := client.Get(endpoint) + if err != nil { + log.Error().Err(err).Str("symbol", symbol).Msg("Failed to fetch Alpha ticker") + return 0, false + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return 0, false + } + + var tickerResp AlphaTickerResponse + if err := json.NewDecoder(resp.Body).Decode(&tickerResp); err != nil { + return 0, false + } + + if tickerResp.Code != "000000" { + return 0, false + } + + price, err := strconv.ParseFloat(tickerResp.Data.Price, 64) + if err != nil || price <= 0 { + return 0, false + } + + return price, true +} diff --git a/internal/services/tele/commands/token.go b/internal/services/tele/commands/token.go index fca9e41..a1be9fa 100644 --- a/internal/services/tele/commands/token.go +++ b/internal/services/tele/commands/token.go @@ -66,9 +66,13 @@ func collectRichTokenData(token string) buildRichTokenMessageArgs { if alphaToken, ok := data.Market.GetAlphaToken(token); ok { a.HasAlpha = true - a.AlphaPrice = alphaToken.GetPrice() a.HasAlpha24h = true a.Alpha24h = alphaToken.GetPercentChange24h() + if alphaPrice, ok := data.Market.GetAlphaPrice(token + "USDT"); ok { + a.AlphaPrice = alphaPrice + } else { + a.AlphaPrice = alphaToken.GetPrice() + } } futureSymbol := token + "USDT" diff --git a/internal/services/tele/commands/token_test.go b/internal/services/tele/commands/token_test.go index 0356891..e8082ef 100644 --- a/internal/services/tele/commands/token_test.go +++ b/internal/services/tele/commands/token_test.go @@ -56,6 +56,7 @@ type marketStub struct { time int64 } alphaTokens map[string]market.AlphaTokenInfo + alphaPrices map[string]float64 marginRates map[string]float64 } @@ -81,6 +82,10 @@ func (m *marketStub) GetAlphaToken(symbol string) (market.AlphaTokenInfo, bool) v, ok := m.alphaTokens[symbol] return v, ok } +func (m *marketStub) GetAlphaPrice(symbol string) (float64, bool) { + v, ok := m.alphaPrices[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 } @@ -126,3 +131,26 @@ func TestCollectRichTokenData_FutureFailureStillKeepsSpot(t *testing.T) { t.Fatalf("did not expect future when future lookup fails: %+v", args) } } + +func TestCollectRichTokenData_AlphaUsesSymbolPrice(t *testing.T) { + orig := data.Market + defer func() { data.Market = orig }() + + data.Market = &marketStub{ + alphaTokens: map[string]market.AlphaTokenInfo{ + "ABC": {Symbol: "ABC", PercentChange24h: "12.4", Price: "0.1"}, + }, + alphaPrices: map[string]float64{"ABCUSDT": 0.1234}, + } + + args := collectRichTokenData("ABC") + if !args.HasAlpha { + t.Fatalf("expected alpha to be available: %+v", args) + } + if !args.HasAlpha24h { + t.Fatalf("expected alpha 24h to be available: %+v", args) + } + if args.AlphaPrice != 0.1234 { + t.Fatalf("expected alpha price from symbol lookup, got %.4f", args.AlphaPrice) + } +} -- 2.52.0