From e1fde342c20a37bd4f47a29e69eb9bf4dd279de9 Mon Sep 17 00:00:00 2001 From: Kyle Zarazan Date: Tue, 4 Oct 2022 11:56:18 -0600 Subject: [PATCH] feat(price-feeder): computed price api routes (#1445) * Save tvwap by provider to oracle * Add api routes and read locks --- price-feeder/CHANGELOG.md | 1 + price-feeder/oracle/convert.go | 5 +-- price-feeder/oracle/oracle.go | 52 +++++++++++++++++---------- price-feeder/oracle/oracle_test.go | 45 +++++++++++------------ price-feeder/oracle/prices.go | 47 ++++++++++++++++++++++++ price-feeder/oracle/util.go | 36 ++++++++++++++++--- price-feeder/oracle/util_test.go | 3 +- price-feeder/router/v1/oracle.go | 3 ++ price-feeder/router/v1/response.go | 5 +++ price-feeder/router/v1/router.go | 31 ++++++++++++++++ price-feeder/router/v1/router_test.go | 49 +++++++++++++++++++++++++ 11 files changed, 224 insertions(+), 53 deletions(-) create mode 100644 price-feeder/oracle/prices.go diff --git a/price-feeder/CHANGELOG.md b/price-feeder/CHANGELOG.md index 0aa017df3a..f17a5aadd1 100644 --- a/price-feeder/CHANGELOG.md +++ b/price-feeder/CHANGELOG.md @@ -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 diff --git a/price-feeder/oracle/convert.go b/price-feeder/oracle/convert.go index 236eb02637..dc67e75834 100644 --- a/price-feeder/oracle/convert.go +++ b/price-feeder/oracle/convert.go @@ -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 diff --git a/price-feeder/oracle/oracle.go b/price-feeder/oracle/oracle.go index 5e1635777a..2435b1be72 100644 --- a/price-feeder/oracle/oracle.go +++ b/price-feeder/oracle/oracle.go @@ -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( @@ -141,8 +144,8 @@ 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 } @@ -150,8 +153,8 @@ func (o *Oracle) GetLastPriceSyncTimestamp() time.Time { // 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)) @@ -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 @@ -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, @@ -262,7 +274,9 @@ func (o *Oracle) SetPrices(ctx context.Context) error { } } + o.pricesMutex.Lock() o.prices = computedPrices + o.pricesMutex.Unlock() return nil } @@ -270,16 +284,16 @@ func (o *Oracle) SetPrices(ctx context.Context) error { // 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, @@ -290,7 +304,7 @@ func GetComputedPrices( // filter out any erroneous candles filteredCandles, err := FilterCandleDeviations( - logger, + o.logger, convertedCandles, deviations, ) @@ -298,6 +312,9 @@ func GetComputedPrices( return nil, err } + computedPrices, _ := ComputeTvwapsByProvider(filteredCandles) + o.tvwapsByProvider.SetPrices(computedPrices) + // attempt to use candles for TVWAP calculations tvwapPrices, err := ComputeTVWAP(filteredCandles) if err != nil { @@ -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, @@ -318,7 +335,7 @@ func GetComputedPrices( } filteredProviderPrices, err := FilterTickerDeviations( - logger, + o.logger, convertedTickers, deviations, ) @@ -326,10 +343,9 @@ func GetComputedPrices( return nil, err } - vwapPrices, err := ComputeVWAP(filteredProviderPrices) - if err != nil { - return nil, err - } + o.vwapsByProvider.SetPrices(ComputeVwapsByProvider(filteredProviderPrices)) + + vwapPrices := ComputeVWAP(filteredProviderPrices) return vwapPrices, nil } diff --git a/price-feeder/oracle/oracle_test.go b/price-feeder/oracle/oracle_test.go index bc85bdea69..256cba4480 100644 --- a/price-feeder/oracle/oracle_test.go +++ b/price-feeder/oracle/oracle_test.go @@ -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", @@ -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", @@ -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", @@ -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", @@ -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" @@ -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) }) } } diff --git a/price-feeder/oracle/prices.go b/price-feeder/oracle/prices.go new file mode 100644 index 0000000000..ff4c5b0124 --- /dev/null +++ b/price-feeder/oracle/prices.go @@ -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 +} diff --git a/price-feeder/oracle/util.go b/price-feeder/oracle/util.go index 403ce56ae6..be7fe850de 100644 --- a/price-feeder/oracle/util.go +++ b/price-feeder/oracle/util.go @@ -17,7 +17,7 @@ const ( ) // compute VWAP for each base by dividing the Σ {P * V} by Σ {V} -func vwap(weightedPrices, volumeSum map[string]sdk.Dec) (map[string]sdk.Dec, error) { +func vwap(weightedPrices, volumeSum map[string]sdk.Dec) map[string]sdk.Dec { vwap := make(map[string]sdk.Dec) for base, p := range weightedPrices { @@ -30,7 +30,7 @@ func vwap(weightedPrices, volumeSum map[string]sdk.Dec) (map[string]sdk.Dec, err } } - return vwap, nil + return vwap } // ComputeVWAP computes the volume weighted average price for all price points @@ -38,7 +38,7 @@ func vwap(weightedPrices, volumeSum map[string]sdk.Dec) (map[string]sdk.Dec, err // of provider => { => , ...}. // // Ref: https://en.wikipedia.org/wiki/Volume-weighted_average_price -func ComputeVWAP(prices provider.AggregatedProviderPrices) (map[string]sdk.Dec, error) { +func ComputeVWAP(prices provider.AggregatedProviderPrices) map[string]sdk.Dec { var ( weightedPrices = make(map[string]sdk.Dec) volumeSum = make(map[string]sdk.Dec) @@ -122,7 +122,7 @@ func ComputeTVWAP(prices provider.AggregatedProviderCandles) (map[string]sdk.Dec } } - return vwap(weightedPrices, volumeSum) + return vwap(weightedPrices, volumeSum), nil } // StandardDeviation returns maps of the standard deviations and means of assets. @@ -184,3 +184,31 @@ func StandardDeviation( return deviations, means, nil } + +// ComputeTvwapsByProvider computes the tvwap prices from candles for each provider separately and returns them +// in a map separated by provider name +func ComputeTvwapsByProvider(prices provider.AggregatedProviderCandles) (map[provider.Name]map[string]sdk.Dec, error) { + tvwaps := make(map[provider.Name]map[string]sdk.Dec) + var err error + + for providerName, candles := range prices { + singleProviderCandles := provider.AggregatedProviderCandles{"providerName": candles} + tvwaps[providerName], err = ComputeTVWAP(singleProviderCandles) + if err != nil { + return nil, err + } + } + return tvwaps, nil +} + +// ComputeVwapsByProvider computes the vwap prices from tickers for each provider separately and returns them +// in a map separated by provider name +func ComputeVwapsByProvider(prices provider.AggregatedProviderPrices) map[provider.Name]map[string]sdk.Dec { + vwaps := make(map[provider.Name]map[string]sdk.Dec) + + for providerName, tickers := range prices { + singleProviderCandles := provider.AggregatedProviderPrices{"providerName": tickers} + vwaps[providerName] = ComputeVWAP(singleProviderCandles) + } + return vwaps +} diff --git a/price-feeder/oracle/util_test.go b/price-feeder/oracle/util_test.go index f29a7072ea..fc50cb5234 100644 --- a/price-feeder/oracle/util_test.go +++ b/price-feeder/oracle/util_test.go @@ -69,8 +69,7 @@ func TestComputeVWAP(t *testing.T) { tc := tc t.Run(name, func(t *testing.T) { - vwap, err := oracle.ComputeVWAP(tc.prices) - require.NoError(t, err) + vwap := oracle.ComputeVWAP(tc.prices) require.Len(t, vwap, len(tc.expected)) for k, v := range tc.expected { diff --git a/price-feeder/router/v1/oracle.go b/price-feeder/router/v1/oracle.go index 54cfb15346..0ad2a31eaa 100644 --- a/price-feeder/router/v1/oracle.go +++ b/price-feeder/router/v1/oracle.go @@ -4,10 +4,13 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/umee-network/umee/price-feeder/oracle" ) // Oracle defines the Oracle interface contract that the v1 router depends on. type Oracle interface { GetLastPriceSyncTimestamp() time.Time GetPrices() map[string]sdk.Dec + GetTvwapPrices() oracle.PricesByProvider + GetVwapPrices() oracle.PricesByProvider } diff --git a/price-feeder/router/v1/response.go b/price-feeder/router/v1/response.go index 261d3b6367..7c0469b6f3 100644 --- a/price-feeder/router/v1/response.go +++ b/price-feeder/router/v1/response.go @@ -6,6 +6,7 @@ import ( "net/http" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/umee-network/umee/price-feeder/oracle/provider" ) // Response constants @@ -27,6 +28,10 @@ type ( PricesResponse struct { Prices map[string]sdk.Dec `json:"prices"` } + + PricesPerProviderResponse struct { + Prices map[provider.Name]map[string]sdk.Dec `json:"providers"` + } ) // errorResponse defines the attributes of a JSON error response. diff --git a/price-feeder/router/v1/router.go b/price-feeder/router/v1/router.go index 232c2ceb42..e02579cf60 100644 --- a/price-feeder/router/v1/router.go +++ b/price-feeder/router/v1/router.go @@ -70,6 +70,16 @@ func (r *Router) RegisterRoutes(rtr *mux.Router, prefix string) { mChain.ThenFunc(r.pricesHandler()), ).Methods(httputil.MethodGET) + v1Router.Handle( + "/prices/providers/tvwap", + mChain.ThenFunc(r.candlePricesHandler()), + ).Methods(httputil.MethodGET) + + v1Router.Handle( + "/prices/providers/vwap", + mChain.ThenFunc(r.tickerPricesHandler()), + ).Methods(httputil.MethodGET) + if r.cfg.Telemetry.Enabled { v1Router.Handle( "/metrics", @@ -100,6 +110,27 @@ func (r *Router) pricesHandler() http.HandlerFunc { } } +func (r *Router) candlePricesHandler() http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + + resp := PricesPerProviderResponse{ + Prices: r.oracle.GetTvwapPrices(), + } + + httputil.RespondWithJSON(w, http.StatusOK, resp) + } +} + +func (r *Router) tickerPricesHandler() http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + resp := PricesPerProviderResponse{ + Prices: r.oracle.GetVwapPrices(), + } + + httputil.RespondWithJSON(w, http.StatusOK, resp) + } +} + func (r *Router) metricsHandler() http.HandlerFunc { return func(w http.ResponseWriter, req *http.Request) { format := strings.TrimSpace(req.FormValue("format")) diff --git a/price-feeder/router/v1/router_test.go b/price-feeder/router/v1/router_test.go index 4dd2a65769..21daabc512 100644 --- a/price-feeder/router/v1/router_test.go +++ b/price-feeder/router/v1/router_test.go @@ -14,6 +14,8 @@ import ( "github.com/cosmos/cosmos-sdk/telemetry" "github.com/umee-network/umee/price-feeder/config" + "github.com/umee-network/umee/price-feeder/oracle" + "github.com/umee-network/umee/price-feeder/oracle/provider" v1 "github.com/umee-network/umee/price-feeder/router/v1" ) @@ -24,6 +26,17 @@ var ( "ATOM": sdk.MustNewDecFromStr("34.84"), "UMEE": sdk.MustNewDecFromStr("4.21"), } + + mockComputedPrices = map[provider.Name]map[string]sdk.Dec{ + provider.ProviderBinance: { + "ATOM": sdk.MustNewDecFromStr("28.21000000"), + "UMEE": sdk.MustNewDecFromStr("1.13000000"), + }, + provider.ProviderKraken: { + "ATOM": sdk.MustNewDecFromStr("28.268700"), + "UMEE": sdk.MustNewDecFromStr("1.13000000"), + }, + } ) type mockOracle struct{} @@ -36,6 +49,14 @@ func (m mockOracle) GetPrices() map[string]sdk.Dec { return mockPrices } +func (m mockOracle) GetTvwapPrices() oracle.PricesByProvider { + return mockComputedPrices +} + +func (m mockOracle) GetVwapPrices() oracle.PricesByProvider { + return mockComputedPrices +} + type mockMetrics struct{} func (mockMetrics) Gather(format string) (telemetry.GatherResponse, error) { @@ -102,3 +123,31 @@ func (rts *RouterTestSuite) TestPrices() { rts.Require().Equal(respBody.Prices["UMEE"], mockPrices["UMEE"]) rts.Require().Equal(respBody.Prices["FOO"], sdk.Dec{}) } + +func (rts *RouterTestSuite) TestTvwap() { + req, err := http.NewRequest("GET", "/api/v1/prices/providers/tvwap", nil) + rts.Require().NoError(err) + response := rts.executeRequest(req) + rts.Require().Equal(http.StatusOK, response.Code) + + var respBody v1.PricesPerProviderResponse + rts.Require().NoError(json.Unmarshal(response.Body.Bytes(), &respBody)) + rts.Require().Equal( + respBody.Prices[provider.ProviderBinance]["ATOM"], + mockComputedPrices[provider.ProviderBinance]["ATOM"], + ) +} + +func (rts *RouterTestSuite) TestVwap() { + req, err := http.NewRequest("GET", "/api/v1/prices/providers/vwap", nil) + rts.Require().NoError(err) + response := rts.executeRequest(req) + rts.Require().Equal(http.StatusOK, response.Code) + + var respBody v1.PricesPerProviderResponse + rts.Require().NoError(json.Unmarshal(response.Body.Bytes(), &respBody)) + rts.Require().Equal( + respBody.Prices[provider.ProviderBinance]["ATOM"], + mockComputedPrices[provider.ProviderBinance]["ATOM"], + ) +}