mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
add simple price resolver
This commit is contained in:
parent
22fa8d5acb
commit
600d81049e
119
pkg/priceresolver/simple.go
Normal file
119
pkg/priceresolver/simple.go
Normal file
|
@ -0,0 +1,119 @@
|
|||
package priceresolver
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
// SimplePriceResolver implements a map-structure-based price index
|
||||
type SimplePriceResolver struct {
|
||||
// symbolPrices stores the latest trade price by mapping symbol to price
|
||||
symbolPrices map[string]fixedpoint.Value
|
||||
markets types.MarketMap
|
||||
|
||||
// pricesByBase stores the prices by currency names as a 2-level map
|
||||
// BTC -> USDT -> 48000.0
|
||||
// BTC -> TWD -> 1536000
|
||||
pricesByBase map[string]map[string]fixedpoint.Value
|
||||
|
||||
// pricesByQuote is for reversed pairs, like USDT/TWD or BNB/BTC
|
||||
// the reason that we don't store the reverse pricing in the same map is:
|
||||
// expression like (1/price) could produce precision issue since the data type is fixed-point, only 8 fraction numbers are supported.
|
||||
pricesByQuote map[string]map[string]fixedpoint.Value
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewPriceMap(markets types.MarketMap) *SimplePriceResolver {
|
||||
return &SimplePriceResolver{
|
||||
markets: markets,
|
||||
symbolPrices: make(map[string]fixedpoint.Value),
|
||||
pricesByBase: make(map[string]map[string]fixedpoint.Value),
|
||||
pricesByQuote: make(map[string]map[string]fixedpoint.Value),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SimplePriceResolver) Update(symbol string, price fixedpoint.Value) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.symbolPrices[symbol] = price
|
||||
market, ok := m.markets[symbol]
|
||||
if !ok {
|
||||
log.Warnf("market info %s not found, unable to update price", symbol)
|
||||
return
|
||||
}
|
||||
|
||||
quoteMap, ok2 := m.pricesByBase[market.BaseCurrency]
|
||||
if !ok2 {
|
||||
quoteMap = make(map[string]fixedpoint.Value)
|
||||
m.pricesByBase[market.BaseCurrency] = quoteMap
|
||||
}
|
||||
|
||||
quoteMap[market.QuoteCurrency] = price
|
||||
|
||||
baseMap, ok3 := m.pricesByQuote[market.QuoteCurrency]
|
||||
if !ok3 {
|
||||
baseMap = make(map[string]fixedpoint.Value)
|
||||
m.pricesByQuote[market.QuoteCurrency] = baseMap
|
||||
}
|
||||
|
||||
baseMap[market.BaseCurrency] = price
|
||||
}
|
||||
|
||||
func (m *SimplePriceResolver) UpdateFromTrade(trade types.Trade) {
|
||||
m.Update(trade.Symbol, trade.Price)
|
||||
}
|
||||
|
||||
func (m *SimplePriceResolver) inferencePrice(asset string, assetPrice fixedpoint.Value, preferredFiats ...string) (fixedpoint.Value, bool) {
|
||||
// log.Infof("inferencePrice %s = %f", asset, assetPrice.Float64())
|
||||
quotePrices, ok := m.pricesByBase[asset]
|
||||
if ok {
|
||||
for quote, price := range quotePrices {
|
||||
for _, fiat := range preferredFiats {
|
||||
if quote == fiat {
|
||||
return price.Mul(assetPrice), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for quote, price := range quotePrices {
|
||||
if infPrice, ok := m.inferencePrice(quote, price.Mul(assetPrice), preferredFiats...); ok {
|
||||
return infPrice, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for example, quote = TWD here, we can get a price map with:
|
||||
// USDT: 32.0 (for USDT/TWD at 32.0)
|
||||
basePrices, ok := m.pricesByQuote[asset]
|
||||
if ok {
|
||||
for base, basePrice := range basePrices {
|
||||
// log.Infof("base %s @ %s", base, basePrice.String())
|
||||
for _, fiat := range preferredFiats {
|
||||
if base == fiat {
|
||||
// log.Infof("ret %f / %f = %f", assetPrice.Float64(), basePrice.Float64(), assetPrice.Div(basePrice).Float64())
|
||||
return assetPrice.Div(basePrice), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for base, basePrice := range basePrices {
|
||||
if infPrice, ok2 := m.inferencePrice(base, assetPrice.Div(basePrice), preferredFiats...); ok2 {
|
||||
return infPrice, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fixedpoint.Zero, false
|
||||
}
|
||||
|
||||
func (m *SimplePriceResolver) ResolvePrice(asset string, preferredFiats ...string) (fixedpoint.Value, bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.inferencePrice(asset, fixedpoint.One, preferredFiats...)
|
||||
}
|
146
pkg/priceresolver/simple_test.go
Normal file
146
pkg/priceresolver/simple_test.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
package priceresolver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/c9s/bbgo/pkg/testing/testhelper"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSimplePriceResolver(t *testing.T) {
|
||||
markets := types.MarketMap{
|
||||
"BTCUSDT": types.Market{
|
||||
BaseCurrency: "BTC",
|
||||
QuoteCurrency: "USDT",
|
||||
},
|
||||
"ETHUSDT": types.Market{
|
||||
BaseCurrency: "ETH",
|
||||
QuoteCurrency: "USDT",
|
||||
},
|
||||
"BTCTWD": types.Market{
|
||||
BaseCurrency: "BTC",
|
||||
QuoteCurrency: "TWD",
|
||||
},
|
||||
"ETHTWD": types.Market{
|
||||
BaseCurrency: "ETH",
|
||||
QuoteCurrency: "TWD",
|
||||
},
|
||||
"USDTTWD": types.Market{
|
||||
BaseCurrency: "USDT",
|
||||
QuoteCurrency: "TWD",
|
||||
},
|
||||
"ETHBTC": types.Market{
|
||||
BaseCurrency: "ETH",
|
||||
QuoteCurrency: "BTC",
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("direct reference", func(t *testing.T) {
|
||||
pm := NewPriceMap(markets)
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "BTCUSDT",
|
||||
Price: Number(48000.0),
|
||||
})
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "ETHUSDT",
|
||||
Price: Number(2800.0),
|
||||
})
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "USDTTWD",
|
||||
Price: Number(32.0),
|
||||
})
|
||||
|
||||
finalPrice, ok := pm.ResolvePrice("BTC", "USDT")
|
||||
if assert.True(t, ok) {
|
||||
assert.Equal(t, "48000", finalPrice.String())
|
||||
}
|
||||
|
||||
finalPrice, ok = pm.ResolvePrice("ETH", "USDT")
|
||||
if assert.True(t, ok) {
|
||||
assert.Equal(t, "2800", finalPrice.String())
|
||||
}
|
||||
|
||||
finalPrice, ok = pm.ResolvePrice("USDT", "TWD")
|
||||
if assert.True(t, ok) {
|
||||
assert.Equal(t, "32", finalPrice.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("simple reference", func(t *testing.T) {
|
||||
pm := NewPriceMap(markets)
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "BTCUSDT",
|
||||
Price: Number(48000.0),
|
||||
})
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "ETHUSDT",
|
||||
Price: Number(2800.0),
|
||||
})
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "USDTTWD",
|
||||
Price: Number(32.0),
|
||||
})
|
||||
|
||||
finalPrice, ok := pm.ResolvePrice("BTC", "TWD")
|
||||
if assert.True(t, ok) {
|
||||
assert.Equal(t, "1536000", finalPrice.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("crypto reference", func(t *testing.T) {
|
||||
pm := NewPriceMap(markets)
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "BTCUSDT",
|
||||
Price: Number(52000.0),
|
||||
})
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "ETHBTC",
|
||||
Price: Number(0.055),
|
||||
})
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "USDTTWD",
|
||||
Price: Number(32.0),
|
||||
})
|
||||
|
||||
finalPrice, ok := pm.ResolvePrice("ETH", "USDT")
|
||||
if assert.True(t, ok) {
|
||||
assert.Equal(t, "2860", finalPrice.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inverse reference", func(t *testing.T) {
|
||||
pm := NewPriceMap(markets)
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "BTCTWD",
|
||||
Price: Number(1536000.0),
|
||||
})
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "USDTTWD",
|
||||
Price: Number(32.0),
|
||||
})
|
||||
|
||||
finalPrice, ok := pm.ResolvePrice("BTC", "USDT")
|
||||
if assert.True(t, ok) {
|
||||
assert.Equal(t, "48000", finalPrice.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("inverse reference", func(t *testing.T) {
|
||||
pm := NewPriceMap(markets)
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "BTCTWD",
|
||||
Price: Number(1536000.0),
|
||||
})
|
||||
pm.UpdateFromTrade(types.Trade{
|
||||
Symbol: "USDTTWD",
|
||||
Price: Number(32.0),
|
||||
})
|
||||
|
||||
finalPrice, ok := pm.ResolvePrice("TWD", "USDT")
|
||||
if assert.True(t, ok) {
|
||||
assert.InDelta(t, 0.03125, finalPrice.Float64(), 0.0001)
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user