Skip to content

Commit

Permalink
feat: Add keeper methods for historacle prices and medians (#1548)
Browse files Browse the repository at this point in the history
* Add keeper methods for historacle prices and medians

* Add HistoraclePricing test

* fix sim-non-determinism

* get rid of getstampperiodhistoricprices

* Update x/oracle/keeper/params.go

Co-authored-by: Robert Zaremba <robert@zaremba.ch>

* Update x/oracle/keeper/params.go

Co-authored-by: Robert Zaremba <robert@zaremba.ch>

* Update x/oracle/keeper/historic_price.go

Co-authored-by: Robert Zaremba <robert@zaremba.ch>

* Update x/oracle/keeper/historic_price.go

Co-authored-by: Robert Zaremba <robert@zaremba.ch>

* Update x/oracle/keeper/historic_price.go

Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>

* Update x/oracle/keeper/historic_price.go

Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>

* Update x/oracle/types/params.go

Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>

* Update x/oracle/keeper/historic_price.go

Co-authored-by: Robert Zaremba <robert@zaremba.ch>

* PR suggestions

* Add median deviation keeper

* gofmt

* Update proto with MedianPeriod and leaner HistoricPrice type

* Add delete methods for median and median deviation

* Update proto/umee/oracle/v1/oracle.proto

Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>

* Update proto/umee/oracle/v1/oracle.proto

Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>

* Update x/oracle/keeper/historic_price.go

Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>

* Update x/oracle/keeper/historic_price.go

Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>

* Make historic price getter private and create error types for medians

* Add method for appending denom and block to key

* Empty-Commit

Co-authored-by: Robert Zaremba <robert@zaremba.ch>
Co-authored-by: Adam Wozniak <29418299+adamewozniak@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 10, 2022
1 parent acad48b commit efd74a6
Show file tree
Hide file tree
Showing 10 changed files with 622 additions and 75 deletions.
10 changes: 7 additions & 3 deletions proto/umee/oracle/v1/oracle.proto
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ message Params {
// Prune Period represents the maximum amount of blocks which we want
// to keep a record of our set of exchange rates.
uint64 prune_period = 10;
// Median Period represents the amount blocks we will wait between
// calculating the median and standard deviation of the median of
// historic prices in the last Prune Period.
uint64 median_period = 11;
}

// Denom - the object to hold configurations of each denom
Expand Down Expand Up @@ -107,9 +111,9 @@ message ExchangeRateTuple {

// HistoricPrice is an instance of a price "stamp"
message HistoricPrice {
ExchangeRateTuple exchange_rates = 1 [
(gogoproto.castrepeated) = "ExchangeRateTuples",
(gogoproto.nullable) = false
ExchangeRateTuple exchange_rate = 1 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false
];
uint64 block_num = 2;
}
205 changes: 205 additions & 0 deletions x/oracle/keeper/historic_price.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package keeper

import (
"fmt"
"sort"

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

"github.com/umee-network/umee/v3/x/oracle/types"
)

// median returns the median of a list of historic prices.
func median(prices []types.HistoricPrice) sdk.Dec {
lenPrices := len(prices)
if lenPrices == 0 {
return sdk.ZeroDec()
}

sort.Slice(prices, func(i, j int) bool {
return prices[i].ExchangeRate.BigInt().
Cmp(prices[j].ExchangeRate.BigInt()) > 0
})

if lenPrices%2 == 0 {
return prices[lenPrices/2-1].ExchangeRate.
Add(prices[lenPrices/2].ExchangeRate).
QuoInt64(2)
}
return prices[lenPrices/2].ExchangeRate
}

// medianDeviation returns the standard deviation around the
// median of a list of prices.
// medianDeviation = ∑((price - median)^2 / len(prices))
func medianDeviation(median sdk.Dec, prices []types.HistoricPrice) sdk.Dec {
lenPrices := len(prices)
medianDeviation := sdk.ZeroDec()

for _, price := range prices {
medianDeviation = medianDeviation.Add(price.ExchangeRate.
Sub(median).Abs().Power(2).
QuoInt64(int64(lenPrices)))
}

return medianDeviation
}

// GetMedian returns a given denom's median price in the last prune
// period since a given block.
func (k Keeper) GetMedian(
ctx sdk.Context,
denom string,
blockNum uint64,
) (sdk.Dec, error) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.GetMedianKey(denom, blockNum))
if bz == nil {
return sdk.ZeroDec(), sdkerrors.Wrap(types.ErrNoMedian, fmt.Sprintf("denom: %s block: %d", denom, blockNum))
}

decProto := sdk.DecProto{}
k.cdc.MustUnmarshal(bz, &decProto)

return decProto.Dec, nil
}

// SetMedian uses all the historic prices of a given denom to calculate
// its median price in the last prune period since the current block and
// set it to the store. It will also call setMedianDeviation with the
// calculated median.
func (k Keeper) SetMedian(
ctx sdk.Context,
denom string,
) {
store := ctx.KVStore(k.storeKey)
historicPrices := k.getHistoricPrices(ctx, denom)
median := median(historicPrices)
bz := k.cdc.MustMarshal(&sdk.DecProto{Dec: median})
store.Set(types.GetMedianKey(denom, uint64(ctx.BlockHeight())), bz)
k.setMedianDeviation(ctx, denom, median, historicPrices)
}

// GetMedianDeviation returns a given denom's standard deviation around
// its median price in the last prune period since a given block.
func (k Keeper) GetMedianDeviation(
ctx sdk.Context,
denom string,
blockNum uint64,
) (sdk.Dec, error) {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.GetMedianDeviationKey(denom, blockNum))
if bz == nil {
return sdk.ZeroDec(), sdkerrors.Wrap(types.ErrNoMedianDeviation, fmt.Sprintf("denom: %s block: %d", denom, blockNum))
}

decProto := sdk.DecProto{}
k.cdc.MustUnmarshal(bz, &decProto)

return decProto.Dec, nil
}

// setMedianDeviation sets a given denom's standard deviation around
// its median price in the last prune period since the current block.
func (k Keeper) setMedianDeviation(
ctx sdk.Context,
denom string,
median sdk.Dec,
prices []types.HistoricPrice,
) {
store := ctx.KVStore(k.storeKey)
medianDeviation := medianDeviation(median, prices)
bz := k.cdc.MustMarshal(&sdk.DecProto{Dec: medianDeviation})
store.Set(types.GetMedianDeviationKey(denom, uint64(ctx.BlockHeight())), bz)
}

// getHistoricPrices returns all the historic prices of a given denom.
func (k Keeper) getHistoricPrices(
ctx sdk.Context,
denom string,
) []types.HistoricPrice {
historicPrices := []types.HistoricPrice{}

k.IterateHistoricPrices(ctx, denom, func(exchangeRate sdk.Dec, blockNum uint64) bool {
historicPrices = append(historicPrices, types.HistoricPrice{
ExchangeRate: exchangeRate,
BlockNum: blockNum,
})

return false
})

return historicPrices
}

// IterateHistoricPrices iterates over historic prices of a given
// denom in the store.
// Iterator stops when exhausting the source, or when the handler returns `true`.
func (k Keeper) IterateHistoricPrices(
ctx sdk.Context,
denom string,
handler func(sdk.Dec, uint64) bool,
) {
store := ctx.KVStore(k.storeKey)

iter := sdk.KVStorePrefixIterator(store, append(types.KeyPrefixHistoricPrice, []byte(denom)...))
defer iter.Close()

for ; iter.Valid(); iter.Next() {
var historicPrice types.HistoricPrice
k.cdc.MustUnmarshal(iter.Value(), &historicPrice)
if handler(historicPrice.ExchangeRate, historicPrice.BlockNum) {
break
}
}
}

// AddHistoricPrice adds the historic price of a denom at the current
// block height.
func (k Keeper) AddHistoricPrice(
ctx sdk.Context,
denom string,
exchangeRate sdk.Dec,
) {
store := ctx.KVStore(k.storeKey)
block := uint64(ctx.BlockHeight())
bz := k.cdc.MustMarshal(&types.HistoricPrice{
ExchangeRate: exchangeRate,
BlockNum: block,
})
store.Set(types.GetHistoricPriceKey(denom, block), bz)
}

// DeleteHistoricPrice deletes the historic price of a denom at a
// given block.
func (k Keeper) DeleteHistoricPrice(
ctx sdk.Context,
denom string,
blockNum uint64,
) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.GetHistoricPriceKey(denom, blockNum))
}

// DeleteMedian deletes a given denom's median price in the last prune
// period since a given block.
func (k Keeper) DeleteMedian(
ctx sdk.Context,
denom string,
blockNum uint64,
) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.GetMedianKey(denom, blockNum))
}

// DeleteMedianDeviation deletes a given denom's standard deviation around
// its median price in the last prune period since a given block.
func (k Keeper) DeleteMedianDeviation(
ctx sdk.Context,
denom string,
blockNum uint64,
) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.GetMedianDeviationKey(denom, blockNum))
}
50 changes: 50 additions & 0 deletions x/oracle/keeper/historic_price_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package keeper_test

import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"

"github.com/umee-network/umee/v3/x/oracle/types"
)

func (s *IntegrationTestSuite) TestSetHistoraclePricing() {
app, ctx := s.app, s.ctx

// set exchange rate in store before adding a historic price
app.OracleKeeper.SetExchangeRate(ctx, displayDenom, sdk.OneDec())
rate, err := app.OracleKeeper.GetExchangeRate(ctx, displayDenom)
s.Require().NoError(err)
s.Require().Equal(rate, sdk.OneDec())

// add multiple historic prices to store
exchangeRates := []string{"1.0", "1.2", "1.1", "1.4"}
for _, exchangeRate := range exchangeRates {
app.OracleKeeper.AddHistoricPrice(ctx, displayDenom, sdk.MustNewDecFromStr(exchangeRate))

// update blockheight
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
}

// set and check median and median standard deviation
app.OracleKeeper.SetMedian(ctx, displayDenom)
median, err := app.OracleKeeper.GetMedian(ctx, displayDenom, uint64(ctx.BlockHeight()))
s.Require().NoError(err)
s.Require().Equal(median, sdk.MustNewDecFromStr("1.15"))

medianDeviation, err := app.OracleKeeper.GetMedianDeviation(ctx, displayDenom, uint64(ctx.BlockHeight()))
s.Require().NoError(err)
s.Require().Equal(medianDeviation, sdk.MustNewDecFromStr("0.0225"))

// delete first historic price, median, and median standard deviation
app.OracleKeeper.DeleteHistoricPrice(ctx, displayDenom, uint64(ctx.BlockHeight()-3))
app.OracleKeeper.DeleteMedian(ctx, displayDenom, uint64(ctx.BlockHeight()))
app.OracleKeeper.DeleteMedianDeviation(ctx, displayDenom, uint64(ctx.BlockHeight()))

median, err = app.OracleKeeper.GetMedian(ctx, displayDenom, uint64(ctx.BlockHeight()))
s.Require().Error(err, sdkerrors.Wrap(types.ErrUnknownDenom, displayDenom))
s.Require().Equal(median, sdk.ZeroDec())

medianDeviation, err = app.OracleKeeper.GetMedianDeviation(ctx, displayDenom, uint64(ctx.BlockHeight()))
s.Require().Error(err, sdkerrors.Wrap(types.ErrUnknownDenom, displayDenom))
s.Require().Equal(median, sdk.ZeroDec())
}
41 changes: 41 additions & 0 deletions x/oracle/keeper/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,47 @@ func (k Keeper) MinValidPerWindow(ctx sdk.Context) (res sdk.Dec) {
return
}

// StampPeriod returns the amount of blocks the oracle module waits
// between recording a set of prices.
func (k Keeper) StampPeriod(ctx sdk.Context) (res uint64) {
k.paramSpace.Get(ctx, types.KeyStampPeriod, &res)
return
}

// SetStampPeriod updates the amount of blocks the oracle module waits
// between recording a set of prices.
func (k Keeper) SetStampPeriod(ctx sdk.Context, stampPeriod uint64) {
k.paramSpace.Set(ctx, types.KeyStampPeriod, stampPeriod)
}

// PrunePeriod returns the max amount of blocks that a record of the set
// of exchanges is kept.
func (k Keeper) PrunePeriod(ctx sdk.Context) (res uint64) {
k.paramSpace.Get(ctx, types.KeyPrunePeriod, &res)
return
}

// SetPrunePeriod updates the max amount of blocks that a record of the set
// of exchanges is kept.
func (k Keeper) SetPrunePeriod(ctx sdk.Context, prunePeriod uint64) {
k.paramSpace.Set(ctx, types.KeyPrunePeriod, prunePeriod)
}

// MedianPeriod returns the amount blocks we will wait between calculating the
// median and standard deviation of the median of historic prices in the
// last Prune Period.
func (k Keeper) MedianPeriod(ctx sdk.Context) (res uint64) {
k.paramSpace.Get(ctx, types.KeyMedianPeriod, &res)
return
}

// MedianPeriod updates the amount blocks we will wait between calculating the
// median and standard deviation of the median of historic prices in the
// last Prune Period.
func (k Keeper) SetMedianPeriod(ctx sdk.Context, medianPeriod uint64) {
k.paramSpace.Set(ctx, types.KeyMedianPeriod, medianPeriod)
}

// GetParams returns the total set of oracle parameters.
func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) {
k.paramSpace.GetParamSet(ctx, &params)
Expand Down
39 changes: 39 additions & 0 deletions x/oracle/simulations/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const (
slashFractionKey = "slash_fraction"
slashWindowKey = "slash_window"
minValidPerWindowKey = "min_valid_per_window"
stampPeriodKey = "stamp_period"
prunePeriodKey = "prune_period"
medianPeriodKey = "median_period"
)

// GenVotePeriod produces a randomized VotePeriod in the range of [5, 100]
Expand Down Expand Up @@ -56,6 +59,21 @@ func GenMinValidPerWindow(r *rand.Rand) sdk.Dec {
return sdk.ZeroDec().Add(sdk.NewDecWithPrec(int64(r.Intn(500)), 3))
}

// GenStampPeriod produces a randomized StampPeriod in the range of [100, 1000]
func GenStampPeriod(r *rand.Rand) uint64 {
return uint64(100 + r.Intn(1000))
}

// GenPrunePeriod produces a randomized PrunePeriod in the range of [10001, 100000]
func GenPrunePeriod(r *rand.Rand) uint64 {
return uint64(10001 + r.Intn(100000))
}

// GenMedianPeriod produces a randomized MedianPeriod in the range of [1001, 10000]
func GenMedianPeriod(r *rand.Rand) uint64 {
return uint64(1001 + r.Intn(10000))
}

// RandomizedGenState generates a random GenesisState for oracle
func RandomizedGenState(simState *module.SimulationState) {
var votePeriod uint64
Expand Down Expand Up @@ -100,6 +118,24 @@ func RandomizedGenState(simState *module.SimulationState) {
func(r *rand.Rand) { minValidPerWindow = GenMinValidPerWindow(r) },
)

var stampPeriod uint64
simState.AppParams.GetOrGenerate(
simState.Cdc, stampPeriodKey, &stampPeriod, simState.Rand,
func(r *rand.Rand) { stampPeriod = GenStampPeriod(r) },
)

var prunePeriod uint64
simState.AppParams.GetOrGenerate(
simState.Cdc, prunePeriodKey, &prunePeriod, simState.Rand,
func(r *rand.Rand) { prunePeriod = GenPrunePeriod(r) },
)

var medianPeriod uint64
simState.AppParams.GetOrGenerate(
simState.Cdc, medianPeriodKey, &medianPeriod, simState.Rand,
func(r *rand.Rand) { medianPeriod = GenMedianPeriod(r) },
)

oracleGenesis := types.NewGenesisState(
types.Params{
VotePeriod: votePeriod,
Expand All @@ -112,6 +148,9 @@ func RandomizedGenState(simState *module.SimulationState) {
SlashFraction: slashFraction,
SlashWindow: slashWindow,
MinValidPerWindow: minValidPerWindow,
StampPeriod: stampPeriod,
PrunePeriod: prunePeriod,
MedianPeriod: medianPeriod,
},
[]types.ExchangeRateTuple{},
[]types.FeederDelegation{},
Expand Down
Loading

0 comments on commit efd74a6

Please sign in to comment.