c8be4c4bfd
Introduce a shared token symbol resolver for spot and futures lookups while keeping alpha lookup independent, restoring prefix/remap handling and preserving spot-only fallback behavior. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
224 lines
5.8 KiB
Go
224 lines
5.8 KiB
Go
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)
|
|
}
|
|
}
|