package commands import ( "testing" "me.thuanle/bbot/internal/data" "me.thuanle/bbot/internal/data/market" ) func TestBuildRichTokenMessageInput_AllSources(t *testing.T) { in := buildRichTokenMessageInput(buildRichTokenMessageArgs{ Token: "ETH", SpotPrice: 3245, HasSpot: true, FuturePrice: 3251, FundingRate: 0.000123, FundingTimeMs: 1740000000000, HasFuture: true, AlphaPrice: 3248, HasAlpha: true, Alpha24h: -3.21, HasAlpha24h: true, MarginAPRPercent: 7.665, HasMarginAPR: true, }) if !in.HasSpot || !in.HasFuture || !in.HasAlpha || !in.HasAlpha24h || !in.HasMarginAPR { t.Fatalf("expected all source flags true: %+v", in) } } func TestBuildRichTokenMessageInput_OnlyAlpha(t *testing.T) { in := buildRichTokenMessageInput(buildRichTokenMessageArgs{ Token: "ABC", HasAlpha: true, AlphaPrice: 0.1234, HasAlpha24h: true, Alpha24h: 12.4, }) if !in.HasAlpha || !in.HasAlpha24h { t.Fatalf("expected alpha flags true: %+v", in) } if in.HasSpot || in.HasFuture { 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 alphaPrices map[string]float64 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) 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 } 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) } } func TestCollectRichTokenData_UsesSharedResolverMapping(t *testing.T) { orig := data.Market defer func() { data.Market = orig }() data.Market = &marketStub{ spotPairs: map[string]bool{ "LUNAUSDT": true, }, futuresPairs: map[string]bool{ "LUNA2USDT": true, }, spotPrices: map[string]float64{ "LUNAUSDT": 0.49, }, futurePrices: map[string]struct { price float64 rate float64 time int64 }{ "LUNA2USDT": {price: 0.50, rate: 0.0001, time: 1740000000000}, }, } args := collectRichTokenData("LUNA2") if !args.HasFuture || !args.HasSpot { t.Fatalf("expected both future and mapped spot, got %+v", args) } if args.SpotPrice != 0.49 { t.Fatalf("expected mapped spot price 0.49, got %f", args.SpotPrice) } } func TestCollectRichTokenData_PrefixFutureMapsToSpot(t *testing.T) { orig := data.Market defer func() { data.Market = orig }() data.Market = &marketStub{ spotPairs: map[string]bool{ "PEPEUSDT": true, }, futuresPairs: map[string]bool{ "1000PEPEUSDT": true, }, spotPrices: map[string]float64{ "PEPEUSDT": 0.000012, }, futurePrices: map[string]struct { price float64 rate float64 time int64 }{ "1000PEPEUSDT": {price: 0.000013, rate: 0.0002, time: 1740000000000}, }, } args := collectRichTokenData("PEPE") if !args.HasFuture || !args.HasSpot { t.Fatalf("expected both future and mapped spot for prefixed contract, got %+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) } }