Pricefeed price simulation refactor (#585)

* refactor price generation to use determistic sequence up to each block
height and reset for each simulation

* remove extra whitespace

* improve comment

* move PriceGenerator to simulation/types to keep logic clean
This commit is contained in:
Nick DeLuca 2020-06-17 21:03:47 -05:00 committed by GitHub
parent e913dc2ff0
commit 70e8f95f02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 112 additions and 84 deletions

View File

@ -2,7 +2,6 @@ package simulation
import ( import (
"math/rand" "math/rand"
"sync"
"time" "time"
"github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/baseapp"
@ -17,13 +16,6 @@ import (
"github.com/kava-labs/kava/x/pricefeed/types" "github.com/kava-labs/kava/x/pricefeed/types"
) )
var (
btcPrices = []sdk.Dec{}
bnbPrices = []sdk.Dec{}
xrpPrices = []sdk.Dec{}
genPrices sync.Once
)
// Simulation operation weights constants // Simulation operation weights constants
const ( const (
OpWeightMsgUpdatePrices = "op_weight_msg_update_prices" OpWeightMsgUpdatePrices = "op_weight_msg_update_prices"
@ -52,39 +44,21 @@ func WeightedOperations(
// SimulateMsgUpdatePrices updates the prices of various assets by randomly varying them based on current price // SimulateMsgUpdatePrices updates the prices of various assets by randomly varying them based on current price
func SimulateMsgUpdatePrices(ak auth.AccountKeeper, keeper keeper.Keeper, blocks int) simulation.Operation { func SimulateMsgUpdatePrices(ak auth.AccountKeeper, keeper keeper.Keeper, blocks int) simulation.Operation {
// runs one at the start of each simulation
startingPrices := map[string]sdk.Dec{
"btc:usd": sdk.MustNewDecFromStr("7000"),
"bnb:usd": sdk.MustNewDecFromStr("15"),
"xrp:usd": sdk.MustNewDecFromStr("0.25"),
}
// creates the new price generator from starting prices - resets for each sim
priceGenerator := NewPriceGenerator(startingPrices)
return func( return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
) (simulation.OperationMsg, []simulation.FutureOperation, error) { ) (simulation.OperationMsg, []simulation.FutureOperation, error) {
// walk prices to current block height, noop if already called for current height
genPrices.Do(func() { priceGenerator.Step(r, ctx.BlockHeight())
// generate a random walk for each asset exactly once, with observations equal to the number of blocks in the sim
for _, m := range keeper.GetMarkets(ctx) {
startPrice := getStartPrice(m.MarketID)
// allow prices to fluctuate from 10x GAINZ to 100x REKT
maxPrice := sdk.MustNewDecFromStr("10.0").Mul(startPrice)
minPrice := sdk.MustNewDecFromStr("0.01").Mul(startPrice)
previousPrice := startPrice
for i := 0; i < blocks; i++ {
increment := getIncrement(m.MarketID)
// note calling r instead of rand here breaks determinism
upDown := rand.Intn(2)
if upDown == 0 {
if previousPrice.Add(increment).GT(maxPrice) {
previousPrice = maxPrice
} else {
previousPrice = previousPrice.Add(increment)
}
} else {
if previousPrice.Sub(increment).LT(minPrice) {
previousPrice = minPrice
} else {
previousPrice = previousPrice.Sub(increment)
}
}
setPrice(m.MarketID, previousPrice)
}
}
})
randomMarket := pickRandomAsset(ctx, keeper, r) randomMarket := pickRandomAsset(ctx, keeper, r)
marketID := randomMarket.MarketID marketID := randomMarket.MarketID
@ -100,7 +74,8 @@ func SimulateMsgUpdatePrices(ak auth.AccountKeeper, keeper keeper.Keeper, blocks
return simulation.NoOpMsg(types.ModuleName), nil, nil return simulation.NoOpMsg(types.ModuleName), nil, nil
} }
price := pickNewRandomPrice(marketID, int(ctx.BlockHeight())) // get price for marketID and current block height set in Step
price := priceGenerator.GetCurrentPrice(marketID)
// get the expiry time based off the current time // get the expiry time based off the current time
expiry := getExpiryTime(ctx) expiry := getExpiryTime(ctx)
@ -132,51 +107,6 @@ func SimulateMsgUpdatePrices(ak auth.AccountKeeper, keeper keeper.Keeper, blocks
} }
} }
func getStartPrice(marketID string) (startPrice sdk.Dec) {
switch marketID {
case "btc:usd":
return sdk.MustNewDecFromStr("7000")
case "bnb:usd":
return sdk.MustNewDecFromStr("15")
case "xrp:usd":
return sdk.MustNewDecFromStr("0.25")
}
return sdk.MustNewDecFromStr("100")
}
func getIncrement(marketID string) (increment sdk.Dec) {
startPrice := getStartPrice(marketID)
divisor := sdk.MustNewDecFromStr("20")
increment = startPrice.Quo(divisor)
return increment
}
func setPrice(marketID string, price sdk.Dec) {
switch marketID {
case "btc:usd":
btcPrices = append(btcPrices, price)
return
case "bnb:usd":
bnbPrices = append(bnbPrices, price)
return
case "xrp:usd":
xrpPrices = append(xrpPrices, price)
}
return
}
func pickNewRandomPrice(marketID string, blockHeight int) (newPrice sdk.Dec) {
switch marketID {
case "btc:usd":
return btcPrices[blockHeight-1]
case "bnb:usd":
return bnbPrices[blockHeight-1]
case "xrp:usd":
return xrpPrices[blockHeight-1]
}
panic("invalid price request")
}
// getRandomOracle picks a random oracle from the list of oracles // getRandomOracle picks a random oracle from the list of oracles
func getRandomOracle(r *rand.Rand, market types.Market) sdk.AccAddress { func getRandomOracle(r *rand.Rand, market types.Market) sdk.AccAddress {
randomIndex := simulation.RandIntBetween(r, 0, len(market.Oracles)) randomIndex := simulation.RandIntBetween(r, 0, len(market.Oracles))

View File

@ -0,0 +1,98 @@
package simulation
import (
"math/rand"
"sort"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// PriceGenerator allows deterministic price generation in simulations
type PriceGenerator struct {
markets []string
currentPrice map[string]sdk.Dec
maxPrice map[string]sdk.Dec
minPrice map[string]sdk.Dec
increment map[string]sdk.Dec
currentBlockHeight int64
}
// NewPriceGenerator returns a new market price generator from starting values
func NewPriceGenerator(startingPrice map[string]sdk.Dec) *PriceGenerator {
p := &PriceGenerator{
markets: []string{},
currentPrice: startingPrice,
maxPrice: map[string]sdk.Dec{},
minPrice: map[string]sdk.Dec{},
increment: map[string]sdk.Dec{},
currentBlockHeight: 0,
}
divisor := sdk.MustNewDecFromStr("20")
for marketID, startPrice := range startingPrice {
p.markets = append(p.markets, marketID)
// allow 10x price increase
p.maxPrice[marketID] = sdk.MustNewDecFromStr("10.0").Mul(startPrice)
// allow 100x price decrease
p.minPrice[marketID] = sdk.MustNewDecFromStr("0.01").Mul(startPrice)
// set increment - should we use a random increment?
p.increment[marketID] = startPrice.Quo(divisor)
}
// market prices must be calculated in a deterministic order
// this sort order defines the the order we update each market
// price in the step function
sort.Strings(p.markets)
return p
}
// Step walks prices to a current block height from the previously called height
// noop if called more than once for the same height
func (p *PriceGenerator) Step(r *rand.Rand, blockHeight int64) {
if p.currentBlockHeight == blockHeight {
// step already called for blockHeight
return
}
if p.currentBlockHeight > blockHeight {
// step is called with a previous blockHeight
panic("step out of order")
}
for _, marketID := range p.markets {
lastPrice := p.currentPrice[marketID]
minPrice := p.minPrice[marketID]
maxPrice := p.maxPrice[marketID]
increment := p.increment[marketID]
lastHeight := p.currentBlockHeight
for lastHeight < blockHeight {
upDown := r.Intn(2)
if upDown == 0 {
lastPrice = sdk.MinDec(lastPrice.Add(increment), maxPrice)
} else {
lastPrice = sdk.MaxDec(lastPrice.Sub(increment), minPrice)
}
lastHeight++
}
p.currentPrice[marketID] = lastPrice
}
p.currentBlockHeight = blockHeight
}
// GetCurrentPrice returns price for last blockHeight set by Step
func (p *PriceGenerator) GetCurrentPrice(marketID string) sdk.Dec {
price, ok := p.currentPrice[marketID]
if !ok {
panic("unknown market")
}
return price
}