Skip to content

Commit

Permalink
feat: faulty provider detection for tvwap (#609)
Browse files Browse the repository at this point in the history
## Description



Adds faulty provider detection logic for tvwap. Also includes some cleanup on the StandardDeviation method & a couple type aliases to make code easier to read. 

progress on: #542

----

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [ ] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added appropriate labels to the PR
- [ ] added `!` to the type prefix if API or client breaking change
- [ ] targeted the correct branch (see [PR Targeting](https://github.com/umee-network/umee/blob/main/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [ ] added a changelog entry to `CHANGELOG.md`
- [ ] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [ ] updated the relevant documentation or specification
- [ ] reviewed "Files changed" and left comments if necessary
- [ ] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
  • Loading branch information
adamewozniak authored Mar 7, 2022
1 parent d75ab49 commit 6b2a099
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 106 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/
- [#580](https://github.com/umee-network/umee/pull/580) Update Kraken provider to use WebSocket.
- [#592](https://github.com/umee-network/umee/pull/592) Add subscribe ticker function to the following providers: Binance, Huobi, Kraken, and Okx.
- [#601](https://github.com/umee-network/umee/pull/601) Use TVWAP formula for determining prices when available.
- [#609](https://github.com/umee-network/umee/pull/609) TVWAP faulty provider detection.

### Bug Fixes

Expand Down
126 changes: 99 additions & 27 deletions price-feeder/oracle/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ const (
tickerTimeout = 1000 * time.Millisecond
)

// deviationThreshold defines how many 𝜎 a provider can be away from the mean
// without being considered faulty.
var deviationThreshold = sdk.MustNewDecFromStr("2")

// PreviousPrevote defines a structure for defining the previous prevote
// submitted on-chain.
type PreviousPrevote struct {
Expand Down Expand Up @@ -152,12 +156,12 @@ func (o *Oracle) GetPrices() map[string]sdk.Dec {
// 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
// with VWAP. Warns the the user of any missing prices, and filters out any faulty
// providers which do not report prices within 2𝜎 of the others.
// providers which do not report prices or candles within 2𝜎 of the others.
func (o *Oracle) SetPrices(ctx context.Context, acceptList oracletypes.DenomList) error {
g := new(errgroup.Group)
mtx := new(sync.Mutex)
providerPrices := make(map[string]map[string]provider.TickerPrice)
providerCandles := make(map[string]map[string][]provider.CandlePrice)
providerPrices := make(provider.AggregatedProviderPrices)
providerCandles := make(provider.AggregatedProviderCandles)
requiredRates := make(map[string]struct{})

for providerName, currencyPairs := range o.providerPairs {
Expand Down Expand Up @@ -249,16 +253,21 @@ func (o *Oracle) SetPrices(ctx context.Context, acceptList oracletypes.DenomList
}
}

filteredCandles, err := o.filterCandleDeviations(providerCandles)
if err != nil {
return err
}

// attempt to use candles for tvwap calculations
tvwapPrices, err := ComputeTVWAP(providerCandles)
tvwapPrices, err := ComputeTVWAP(filteredCandles)
if err != nil {
return err
}

// If TVWAP candles are not available or were filtered out due to staleness,
// use most recent prices & VWAP instead.
if len(tvwapPrices) == 0 {
filteredProviderPrices, err := o.filterDeviations(providerPrices)
filteredProviderPrices, err := o.filterTickerDeviations(providerPrices)
if err != nil {
return err
}
Expand Down Expand Up @@ -352,47 +361,110 @@ func (o *Oracle) getOrSetProvider(ctx context.Context, providerName string) (pro
return priceProvider, nil
}

// filterDeviations find the standard deviations of the prices of
// all assets, and filter out any providers that are not within 2𝜎 of the mean.
func (o *Oracle) filterDeviations(
prices map[string]map[string]provider.TickerPrice) (
map[string]map[string]provider.TickerPrice, error,
) {
// filterTickerDeviations finds the standard deviations of the prices of
// all assets, and filters out any providers that are not within 2𝜎 of the mean.
func (o *Oracle) filterTickerDeviations(
prices provider.AggregatedProviderPrices,
) (provider.AggregatedProviderPrices, error) {
var (
filteredPrices = make(map[string]map[string]provider.TickerPrice)
threshold = sdk.MustNewDecFromStr("2")
filteredPrices = make(provider.AggregatedProviderPrices)
priceMap = make(map[string]map[string]sdk.Dec)
)

deviations, means, err := StandardDeviation(prices)
for providerName, providerPrices := range prices {
if _, ok := priceMap[providerName]; !ok {
priceMap[providerName] = make(map[string]sdk.Dec)
}
for base, price := range providerPrices {
priceMap[providerName][base] = price.Price
}
}

deviations, means, err := StandardDeviation(priceMap)
if err != nil {
return make(map[string]map[string]provider.TickerPrice), nil
return nil, err
}

// Accept any prices that are within 2𝜎, or for which we couldn't get 𝜎
for providerName, priceMap := range prices {
for base, price := range priceMap {
// accept any prices that are within 2𝜎, or for which we couldn't get 𝜎
for providerName, priceTickers := range prices {
for base, ticker := range priceTickers {
if _, ok := deviations[base]; !ok ||
(price.Price.GTE(means[base].Sub(deviations[base].Mul(threshold))) &&
price.Price.LTE(means[base].Add(deviations[base].Mul(threshold)))) {
(ticker.Price.GTE(means[base].Sub(deviations[base].Mul(deviationThreshold))) &&
ticker.Price.LTE(means[base].Add(deviations[base].Mul(deviationThreshold)))) {
if _, ok := filteredPrices[providerName]; !ok {
filteredPrices[providerName] = make(map[string]provider.TickerPrice)
}
filteredPrices[providerName][base] = provider.TickerPrice{
Price: price.Price,
Volume: price.Volume,
}
filteredPrices[providerName][base] = ticker
} else {
telemetry.IncrCounter(1, "failure", "provider")
o.logger.Warn().Str("base", base).Str("provider", providerName).Msg(
"provider deviating from other prices",
)
o.logger.Warn().Str("base", base).Str("provider", providerName).Str(
"price", ticker.Price.String()).Msg("provider deviating from other prices")
}
}
}

return filteredPrices, nil
}

// filterCandleDeviations finds the standard deviations of the tvwaps of
// all assets, and filters out any providers that are not within 2𝜎 of the mean.
func (o *Oracle) filterCandleDeviations(
candles provider.AggregatedProviderCandles,
) (provider.AggregatedProviderCandles, error) {
var (
filteredCandles = make(provider.AggregatedProviderCandles)
tvwaps = make(map[string]map[string]sdk.Dec)
)

for providerName, c := range candles {
candlePrices := make(provider.AggregatedProviderCandles)

for assetName, asset := range c {
if _, ok := candlePrices[providerName]; !ok {
candlePrices[providerName] = make(map[string][]provider.CandlePrice)
}
candlePrices[providerName][assetName] = asset
}

tvwap, err := ComputeTVWAP(candlePrices)
if err != nil {
return nil, err
}

for assetName, asset := range tvwap {
if _, ok := tvwaps[providerName]; !ok {
tvwaps[providerName] = make(map[string]sdk.Dec)
}
tvwaps[providerName][assetName] = asset
}
}

deviations, means, err := StandardDeviation(tvwaps)
if err != nil {
return nil, err
}

// accept any tvwaps that are within 2𝜎, or for which we couldn't get 𝜎
for providerName, priceMap := range tvwaps {
for base, price := range priceMap {
if _, ok := deviations[base]; !ok ||
(price.GTE(means[base].Sub(deviations[base].Mul(deviationThreshold))) &&
price.LTE(means[base].Add(deviations[base].Mul(deviationThreshold)))) {
if _, ok := filteredCandles[providerName]; !ok {
filteredCandles[providerName] = make(map[string][]provider.CandlePrice)
}
filteredCandles[providerName][base] = candles[providerName][base]
} else {
telemetry.IncrCounter(1, "failure", "provider")
o.logger.Warn().Str("base", base).Str("provider", providerName).Str(
"price", price.String()).Msg("provider deviating from other candles")
}
}
}

return filteredCandles, nil
}

func (o *Oracle) checkAcceptList(params oracletypes.Params) {
for _, denom := range params.AcceptList {
symbol := strings.ToUpper(denom.SymbolDenom)
Expand Down
8 changes: 8 additions & 0 deletions price-feeder/oracle/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type TickerPrice struct {
Volume sdk.Dec // 24h volume
}

// AggregatedProviderPrices defines a type alias for a map
// of provider -> asset -> TickerPrice
type AggregatedProviderPrices map[string]map[string]TickerPrice

// CandlePrice defines price, volume, and time information for an
// exchange rate.
type CandlePrice struct {
Expand All @@ -41,6 +45,10 @@ type CandlePrice struct {
TimeStamp int64 // timestamp
}

// AggregatedProviderCandles defines a type alias for a map
// of provider -> asset -> []CandlePrice
type AggregatedProviderCandles map[string]map[string][]CandlePrice

// preventRedirect avoid any redirect in the http.Client the request call
// will not return an error, but a valid response with redirect response code.
func preventRedirect(_ *http.Request, _ []*http.Request) error {
Expand Down
10 changes: 5 additions & 5 deletions price-feeder/oracle/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func vwap(weightedPrices map[string]sdk.Dec, volumeSum map[string]sdk.Dec) (map[
// of provider => {<base> => <TickerPrice>, ...}.
//
// Ref: https://en.wikipedia.org/wiki/Volume-weighted_average_price
func ComputeVWAP(prices map[string]map[string]provider.TickerPrice) (map[string]sdk.Dec, error) {
func ComputeVWAP(prices provider.AggregatedProviderPrices) (map[string]sdk.Dec, error) {
var (
weightedPrices = make(map[string]sdk.Dec)
volumeSum = make(map[string]sdk.Dec)
Expand Down Expand Up @@ -68,7 +68,7 @@ func ComputeVWAP(prices map[string]map[string]provider.TickerPrice) (map[string]
// provider => {<base> => <TickerPrice>, ...}.
//
// Ref : https://en.wikipedia.org/wiki/Time-weighted_average_price
func ComputeTVWAP(prices map[string]map[string][]provider.CandlePrice) (map[string]sdk.Dec, error) {
func ComputeTVWAP(prices provider.AggregatedProviderCandles) (map[string]sdk.Dec, error) {
var (
weightedPrices = make(map[string]sdk.Dec)
volumeSum = make(map[string]sdk.Dec)
Expand Down Expand Up @@ -121,7 +121,7 @@ func ComputeTVWAP(prices map[string]map[string][]provider.CandlePrice) (map[stri
// StandardDeviation returns maps of the standard deviations and means of assets.
// Will skip calculating for an asset if there are less than 3 prices.
func StandardDeviation(
prices map[string]map[string]provider.TickerPrice) (
prices map[string]map[string]sdk.Dec) (
map[string]sdk.Dec, map[string]sdk.Dec, error,
) {
var (
Expand All @@ -140,8 +140,8 @@ func StandardDeviation(
priceSlice[base] = []sdk.Dec{}
}

priceSums[base] = priceSums[base].Add(tp.Price)
priceSlice[base] = append(priceSlice[base], tp.Price)
priceSums[base] = priceSums[base].Add(tp)
priceSlice[base] = append(priceSlice[base], tp)
}
}

Expand Down
103 changes: 29 additions & 74 deletions price-feeder/oracle/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,75 +85,47 @@ func TestStandardDeviation(t *testing.T) {
deviation sdk.Dec
}
testCases := map[string]struct {
prices map[string]map[string]provider.TickerPrice
prices map[string]map[string]sdk.Dec
expected map[string]deviation
}{
"empty prices": {
prices: make(map[string]map[string]provider.TickerPrice),
prices: make(map[string]map[string]sdk.Dec),
expected: map[string]deviation{},
},
"nil prices": {
prices: nil,
expected: map[string]deviation{},
},
"not enough prices": {
prices: map[string]map[string]provider.TickerPrice{
prices: map[string]map[string]sdk.Dec{
config.ProviderBinance: {
"ATOM": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("28.21000000"),
},
"UMEE": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("1.13000000"),
},
"LUNA": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("64.87000000"),
},
"ATOM": sdk.MustNewDecFromStr("28.21000000"),
"UMEE": sdk.MustNewDecFromStr("1.13000000"),
"LUNA": sdk.MustNewDecFromStr("64.87000000"),
},
config.ProviderKraken: {
"ATOM": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("28.23000000"),
},
"UMEE": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("1.13050000"),
},
"LUNA": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("64.85000000"),
},
"ATOM": sdk.MustNewDecFromStr("28.23000000"),
"UMEE": sdk.MustNewDecFromStr("1.13050000"),
"LUNA": sdk.MustNewDecFromStr("64.85000000"),
},
},
expected: map[string]deviation{},
},
"some prices": {
prices: map[string]map[string]provider.TickerPrice{
prices: map[string]map[string]sdk.Dec{
config.ProviderBinance: {
"ATOM": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("28.21000000"),
},
"UMEE": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("1.13000000"),
},
"LUNA": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("64.87000000"),
},
"ATOM": sdk.MustNewDecFromStr("28.21000000"),
"UMEE": sdk.MustNewDecFromStr("1.13000000"),
"LUNA": sdk.MustNewDecFromStr("64.87000000"),
},
config.ProviderKraken: {
"ATOM": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("28.23000000"),
},
"UMEE": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("1.13050000"),
},
"ATOM": sdk.MustNewDecFromStr("28.23000000"),
"UMEE": sdk.MustNewDecFromStr("1.13050000"),
},
config.ProviderOsmosis: {
"ATOM": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("28.40000000"),
},
"UMEE": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("1.14000000"),
},
"LUNA": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("64.10000000"),
},
"ATOM": sdk.MustNewDecFromStr("28.40000000"),
"UMEE": sdk.MustNewDecFromStr("1.14000000"),
"LUNA": sdk.MustNewDecFromStr("64.10000000"),
},
},
expected: map[string]deviation{
Expand All @@ -169,39 +141,22 @@ func TestStandardDeviation(t *testing.T) {
},

"non empty prices": {
prices: map[string]map[string]provider.TickerPrice{
prices: map[string]map[string]sdk.Dec{
config.ProviderBinance: {
"ATOM": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("28.21000000"),
},
"UMEE": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("1.13000000"),
},
"LUNA": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("64.87000000"),
},
"ATOM": sdk.MustNewDecFromStr("28.21000000"),

"UMEE": sdk.MustNewDecFromStr("1.13000000"),
"LUNA": sdk.MustNewDecFromStr("64.87000000"),
},
config.ProviderKraken: {
"ATOM": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("28.23000000"),
},
"UMEE": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("1.13050000"),
},
"LUNA": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("64.85000000"),
},
"ATOM": sdk.MustNewDecFromStr("28.23000000"),
"UMEE": sdk.MustNewDecFromStr("1.13050000"),
"LUNA": sdk.MustNewDecFromStr("64.85000000"),
},
config.ProviderOsmosis: {
"ATOM": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("28.40000000"),
},
"UMEE": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("1.14000000"),
},
"LUNA": provider.TickerPrice{
Price: sdk.MustNewDecFromStr("64.10000000"),
},
"ATOM": sdk.MustNewDecFromStr("28.40000000"),
"UMEE": sdk.MustNewDecFromStr("1.14000000"),
"LUNA": sdk.MustNewDecFromStr("64.10000000"),
},
},
expected: map[string]deviation{
Expand Down

0 comments on commit 6b2a099

Please sign in to comment.