Skip to content

Commit

Permalink
feat(price-feeder): computed price api routes (#1445)
Browse files Browse the repository at this point in the history
* Save tvwap by provider to oracle
* Add api routes and read locks
  • Loading branch information
zarazan authored Oct 4, 2022
1 parent f805484 commit e1fde34
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 53 deletions.
1 change: 1 addition & 0 deletions price-feeder/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Ref: https://keepachangelog.com/en/1.0.0/

- [1328](https://github.com/umee-network/umee/pull/1328) Add bitget provider.
- [1339](https://github.com/umee-network/umee/pull/1339) Add mexc provider.
- [1445](https://github.com/umee-network/umee/pull/1445) Add computed prices api endpoints for debugging.

### Bugs

Expand Down
5 changes: 1 addition & 4 deletions price-feeder/oracle/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,7 @@ func convertTickersToUSD(
return nil, err
}

vwap, err := ComputeVWAP(filteredTickers)
if err != nil {
return nil, err
}
vwap := ComputeVWAP(filteredTickers)

conversionRates[pair.Quote] = vwap[pair.Quote]
requiredConversions[pairProviderName] = pair
Expand Down
52 changes: 34 additions & 18 deletions price-feeder/oracle/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,14 @@ type Oracle struct {
oracleClient client.OracleClient
deviations map[string]sdk.Dec
endpoints map[provider.Name]provider.Endpoint
paramCache ParamCache

mtx sync.RWMutex
pricesMutex sync.RWMutex
lastPriceSyncTS time.Time
prices map[string]sdk.Dec
paramCache ParamCache

tvwapsByProvider PricesWithMutex
vwapsByProvider PricesWithMutex
}

func New(
Expand Down Expand Up @@ -141,17 +144,17 @@ func (o *Oracle) Stop() {
// GetLastPriceSyncTimestamp returns the latest timestamp at which prices where
// fetched from the oracle's set of exchange rate providers.
func (o *Oracle) GetLastPriceSyncTimestamp() time.Time {
o.mtx.RLock()
defer o.mtx.RUnlock()
o.pricesMutex.RLock()
defer o.pricesMutex.RUnlock()

return o.lastPriceSyncTS
}

// GetPrices returns a copy of the current prices fetched from the oracle's
// set of exchange rate providers.
func (o *Oracle) GetPrices() map[string]sdk.Dec {
o.mtx.RLock()
defer o.mtx.RUnlock()
o.pricesMutex.RLock()
defer o.pricesMutex.RUnlock()

// Creates a new array for the prices in the oracle
prices := make(map[string]sdk.Dec, len(o.prices))
Expand All @@ -163,6 +166,16 @@ func (o *Oracle) GetPrices() map[string]sdk.Dec {
return prices
}

// GetTvwapPrices returns a copy of the tvwapsByProvider map
func (o *Oracle) GetTvwapPrices() PricesByProvider {
return o.tvwapsByProvider.GetPricesClone()
}

// GetVwapPrices returns the vwapsByProvider map using a read lock
func (o *Oracle) GetVwapPrices() PricesByProvider {
return o.vwapsByProvider.GetPricesClone()
}

// SetPrices retrieves all the prices and candles from our set of providers as
// determined in the config. If candles are available, uses TVWAP in order
// to determine prices. If candles are not available, uses the most recent prices
Expand Down Expand Up @@ -242,8 +255,7 @@ func (o *Oracle) SetPrices(ctx context.Context) error {
o.logger.Err(err).Msg("failed to get ticker prices from provider")
}

computedPrices, err := GetComputedPrices(
o.logger,
computedPrices, err := o.GetComputedPrices(
providerCandles,
providerPrices,
o.providerPairs,
Expand All @@ -262,24 +274,26 @@ func (o *Oracle) SetPrices(ctx context.Context) error {
}
}

o.pricesMutex.Lock()
o.prices = computedPrices
o.pricesMutex.Unlock()
return nil
}

// GetComputedPrices gets the candle and ticker prices and computes it.
// It returns candles' TVWAP if possible, if not possible (not available
// or due to some staleness) it will use the most recent ticker prices
// and the VWAP formula instead.
func GetComputedPrices(
logger zerolog.Logger,
func (o *Oracle) GetComputedPrices(
providerCandles provider.AggregatedProviderCandles,
providerPrices provider.AggregatedProviderPrices,
providerPairs map[provider.Name][]types.CurrencyPair,
deviations map[string]sdk.Dec,
) (prices map[string]sdk.Dec, err error) {

// convert any non-USD denominated candles into USD
convertedCandles, err := convertCandlesToUSD(
logger,
o.logger,
providerCandles,
providerPairs,
deviations,
Expand All @@ -290,14 +304,17 @@ func GetComputedPrices(

// filter out any erroneous candles
filteredCandles, err := FilterCandleDeviations(
logger,
o.logger,
convertedCandles,
deviations,
)
if err != nil {
return nil, err
}

computedPrices, _ := ComputeTvwapsByProvider(filteredCandles)
o.tvwapsByProvider.SetPrices(computedPrices)

// attempt to use candles for TVWAP calculations
tvwapPrices, err := ComputeTVWAP(filteredCandles)
if err != nil {
Expand All @@ -308,7 +325,7 @@ func GetComputedPrices(
// use most recent prices & VWAP instead.
if len(tvwapPrices) == 0 {
convertedTickers, err := convertTickersToUSD(
logger,
o.logger,
providerPrices,
providerPairs,
deviations,
Expand All @@ -318,18 +335,17 @@ func GetComputedPrices(
}

filteredProviderPrices, err := FilterTickerDeviations(
logger,
o.logger,
convertedTickers,
deviations,
)
if err != nil {
return nil, err
}

vwapPrices, err := ComputeVWAP(filteredProviderPrices)
if err != nil {
return nil, err
}
o.vwapsByProvider.SetPrices(ComputeVwapsByProvider(filteredProviderPrices))

vwapPrices := ComputeVWAP(filteredProviderPrices)

return vwapPrices, nil
}
Expand Down
45 changes: 20 additions & 25 deletions price-feeder/oracle/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ func TestFailedSetProviderTickerPricesAndCandles(t *testing.T) {
require.False(t, success, "It should failed to set the prices, prices and candle are empty")
}

func TestSuccessGetComputedPricesCandles(t *testing.T) {
func (ots *OracleTestSuite) TestSuccessGetComputedPricesCandles() {
providerCandles := make(provider.AggregatedProviderCandles, 1)
pair := types.CurrencyPair{
Base: "ATOM",
Expand All @@ -467,19 +467,18 @@ func TestSuccessGetComputedPricesCandles(t *testing.T) {
provider.ProviderBinance: {pair},
}

prices, err := GetComputedPrices(
zerolog.Nop(),
prices, err := ots.oracle.GetComputedPrices(
providerCandles,
make(provider.AggregatedProviderPrices, 1),
providerPair,
make(map[string]sdk.Dec),
)

require.NoError(t, err, "It should successfully get computed candle prices")
require.Equal(t, prices[pair.Base], atomPrice)
require.NoError(ots.T(), err, "It should successfully get computed candle prices")
require.Equal(ots.T(), prices[pair.Base], atomPrice)
}

func TestSuccessGetComputedPricesTickers(t *testing.T) {
func (ots *OracleTestSuite) TestSuccessGetComputedPricesTickers() {
providerPrices := make(provider.AggregatedProviderPrices, 1)
pair := types.CurrencyPair{
Base: "ATOM",
Expand All @@ -500,19 +499,18 @@ func TestSuccessGetComputedPricesTickers(t *testing.T) {
provider.ProviderBinance: {pair},
}

prices, err := GetComputedPrices(
zerolog.Nop(),
prices, err := ots.oracle.GetComputedPrices(
make(provider.AggregatedProviderCandles, 1),
providerPrices,
providerPair,
make(map[string]sdk.Dec),
)

require.NoError(t, err, "It should successfully get computed ticker prices")
require.Equal(t, prices[pair.Base], atomPrice)
require.NoError(ots.T(), err, "It should successfully get computed ticker prices")
require.Equal(ots.T(), prices[pair.Base], atomPrice)
}

func TestGetComputedPricesCandlesConversion(t *testing.T) {
func (ots *OracleTestSuite) TestGetComputedPricesCandlesConversion() {
btcPair := types.CurrencyPair{
Base: "BTC",
Quote: "ETH",
Expand Down Expand Up @@ -596,25 +594,24 @@ func TestGetComputedPricesCandlesConversion(t *testing.T) {
provider.ProviderKraken: {btcUSDPair},
}

prices, err := GetComputedPrices(
zerolog.Nop(),
prices, err := ots.oracle.GetComputedPrices(
providerCandles,
make(provider.AggregatedProviderPrices, 1),
providerPair,
make(map[string]sdk.Dec),
)

require.NoError(t, err,
require.NoError(ots.T(), err,
"It should successfully filter out bad candles and convert everything to USD",
)
require.Equal(t,
require.Equal(ots.T(),
ethUsdPrice.Mul(
btcEthPrice).Add(btcUSDPrice).Quo(sdk.MustNewDecFromStr("2")),
prices[btcPair.Base],
)
}

func TestGetComputedPricesTickersConversion(t *testing.T) {
func (ots *OracleTestSuite) TestGetComputedPricesTickersConversion() {
btcPair := types.CurrencyPair{
Base: "BTC",
Quote: "ETH",
Expand Down Expand Up @@ -680,25 +677,24 @@ func TestGetComputedPricesTickersConversion(t *testing.T) {
provider.ProviderKraken: {btcUSDPair},
}

prices, err := GetComputedPrices(
zerolog.Nop(),
prices, err := ots.oracle.GetComputedPrices(
make(provider.AggregatedProviderCandles, 1),
providerPrices,
providerPair,
make(map[string]sdk.Dec),
)

require.NoError(t, err,
require.NoError(ots.T(), err,
"It should successfully filter out bad tickers and convert everything to USD",
)
require.Equal(t,
require.Equal(ots.T(),
ethUsdPrice.Mul(
btcEthPrice).Add(btcUSDPrice).Quo(sdk.MustNewDecFromStr("2")),
prices[btcPair.Base],
)
}

func TestGetComputedPricesEmptyTvwap(t *testing.T) {
func (ots *OracleTestSuite) TestGetComputedPricesEmptyTvwap() {
symbolUSDT := "USDT"
symbolUSD := "USD"
symbolDAI := "DAI"
Expand Down Expand Up @@ -802,16 +798,15 @@ func TestGetComputedPricesEmptyTvwap(t *testing.T) {
for name, tc := range testCases {
tc := tc

t.Run(name, func(t *testing.T) {
_, err := GetComputedPrices(
zerolog.Nop(),
ots.Run(name, func() {
_, err := ots.oracle.GetComputedPrices(
tc.candles,
tc.prices,
tc.pairs,
make(map[string]sdk.Dec),
)

require.ErrorContains(t, err, tc.expected)
require.ErrorContains(ots.T(), err, tc.expected)
})
}
}
47 changes: 47 additions & 0 deletions price-feeder/oracle/prices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package oracle

import (
"sync"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/umee-network/umee/price-feeder/oracle/provider"
)

type (
PricesByProvider map[provider.Name]map[string]sdk.Dec

PricesWithMutex struct {
prices PricesByProvider
mx sync.RWMutex
}
)

// SetPrices sets the PricesWithMutex.prices value surrounded by a write lock
func (pwm *PricesWithMutex) SetPrices(prices PricesByProvider) {
pwm.mx.Lock()
defer pwm.mx.Unlock()

pwm.prices = prices
}

// GetPricesClone retrieves a clone of PricesWithMutex.prices
// surrounded by a read lock
func (pwm *PricesWithMutex) GetPricesClone() PricesByProvider {
pwm.mx.RLock()
defer pwm.mx.RUnlock()
return pwm.clonePrices()
}

// clonePrices returns a deep copy of PricesWithMutex.prices
func (pwm *PricesWithMutex) clonePrices() PricesByProvider {
clone := make(PricesByProvider, len(pwm.prices))
for provider, prices := range pwm.prices {
pricesClone := make(map[string]sdk.Dec, len(prices))
for denom, price := range prices {
pricesClone[denom] = price
}
clone[provider] = pricesClone
}
return clone
}
Loading

0 comments on commit e1fde34

Please sign in to comment.