Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pricefeed): TWAP in query price #811

Merged
merged 12 commits into from
Aug 15, 2022
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Features

* [#791](https://github.com/NibiruChain/nibiru/pull/791) Add the x/oracle module
- [#813](https://github.com/NibiruChain/nibiru/pull/813) - (vpool): Expose mark price, mark TWAP, index price, and k (swap invariant) in the all-pools query
* [#811](https://github.com/NibiruChain/nibiru/pull/811) Return the index twap in `QueryPrice` cmd
* [#813](https://github.com/NibiruChain/nibiru/pull/813) - (vpool): Expose mark price, mark TWAP, index price, and k (swap invariant) in the all-pools query
* [#810](https://github.com/NibiruChain/nibiru/pull/810) - feat(x/perp): expose 'marginRatioIndex' and block number on QueryTraderPosition

## [v0.12.1](https://github.com/NibiruChain/nibiru/releases/tag/v0.12.1) - 2022-08-04
Expand Down
2 changes: 1 addition & 1 deletion proto/oracle/v1beta1/oracle.proto
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ message Pair {

// struct for aggregate prevoting on the ExchangeRateVote.
// The purpose of aggregate prevote is to hide vote exchange rates with hash
// which is formatted as hex string in SHA256("{salt}:({pair},{exchange_rate})|...,({pair},{exchange_rate}):{voter}")
// which is formatted as hex string in SHA256("{salt}:({pair},{exchange_rate})|...|({pair},{exchange_rate}):{voter}")
message AggregateExchangeRatePrevote {
option (gogoproto.equal) = false;
option (gogoproto.goproto_getters) = false;
Expand Down
5 changes: 5 additions & 0 deletions proto/pricefeed/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,12 @@ message PostedPriceResponse {
// module.
message CurrentPriceResponse {
string pair_id = 1 [(gogoproto.customname) = "PairID"];

// most current price of the trading pair
string price = 2 [(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false];

// twap of the trading pair
string twap = 3 [(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false];
}

// Market defines an asset in the pricefeed.
Expand Down
46 changes: 25 additions & 21 deletions x/pricefeed/client/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,15 @@ const (
type IntegrationTestSuite struct {
suite.Suite

cfg testutilcli.Config
network *testutilcli.Network
oracleUIDs []string
oracleMap map[string]sdk.AccAddress
cfg testutilcli.Config
network *testutilcli.Network
oracleMap map[string]sdk.AccAddress
}

func (s *IntegrationTestSuite) setupOraclesForKeyring() {
val := s.network.Validators[0]
s.oracleUIDs = []string{"oracle", "wrongOracle"}

for _, oracleUID := range s.oracleUIDs {
for _, oracleUID := range []string{"oracle", "wrongOracle"} {
info, _, err := val.ClientCtx.Keyring.NewMnemonic(
/* uid */ oracleUID,
/* language */ keyring.English,
Expand Down Expand Up @@ -234,6 +232,7 @@ func (s IntegrationTestSuite) TestGetRawPricesCmd() {
})
}
}

func expireWithinHours(t time.Time, hours time.Duration) bool {
now := time.Now()
return t.After(now) && t.Before(now.Add(hours*time.Hour))
Expand Down Expand Up @@ -283,20 +282,29 @@ func (s IntegrationTestSuite) TestPairsCmd() {
})
}
}

func (s IntegrationTestSuite) TestPricesCmd() {
val := s.network.Validators[0]

testCases := []struct {
name string

expectedPricePairs []pricefeedtypes.CurrentPriceResponse
respType proto.Message
expectedPrices []pricefeedtypes.CurrentPriceResponse
respType proto.Message
}{
{
name: "Get current prices",
expectedPricePairs: []pricefeedtypes.CurrentPriceResponse{
pricefeedtypes.NewCurrentPriceResponse(common.PairGovStable.String(), sdk.NewDec(10)),
pricefeedtypes.NewCurrentPriceResponse(common.PairCollStable.String(), sdk.NewDec(1)),
expectedPrices: []pricefeedtypes.CurrentPriceResponse{
{
PairID: common.PairGovStable.String(),
Price: sdk.NewDec(10),
Twap: sdk.ZeroDec(),
},
{
PairID: common.PairCollStable.String(),
Price: sdk.NewDec(1),
Twap: sdk.ZeroDec(),
},
},
respType: &pricefeedtypes.QueryPricesResponse{},
},
Expand All @@ -311,15 +319,13 @@ func (s IntegrationTestSuite) TestPricesCmd() {

out, err := sdktestutilcli.ExecTestCLICmd(clientCtx, cmd, nil)
s.Require().NoError(err, out.String())
s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())

txResp := tc.respType.(*pricefeedtypes.QueryPricesResponse)
err = val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), txResp)
s.Require().NoError(err)
s.Assert().Equal(len(tc.expectedPricePairs), len(txResp.Prices))
s.Require().NoError(val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), txResp), out.String())
s.Assert().Equal(len(tc.expectedPrices), len(txResp.Prices))

for _, priceResponse := range txResp.Prices {
s.Assert().Contains(tc.expectedPricePairs, priceResponse, tc.expectedPricePairs)
s.Assert().Contains(tc.expectedPrices, priceResponse, tc.expectedPrices)
}
})
}
Expand Down Expand Up @@ -531,16 +537,14 @@ func (s IntegrationTestSuite) TestGetParamsCmd() {
tc := tc

s.Run(tc.name, func() {
cmd := cli.CmdQueryParams()
clientCtx := val.ClientCtx.WithOutputFormat("json")

out, err := sdktestutilcli.ExecTestCLICmd(clientCtx, cmd, nil)
out, err := sdktestutilcli.ExecTestCLICmd(clientCtx, cli.CmdQueryParams(), nil)
s.Require().NoError(err, out.String())
s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), tc.respType), out.String())

txResp := tc.respType.(*pricefeedtypes.QueryParamsResponse)
err = val.ClientCtx.Codec.UnmarshalJSON(out.Bytes(), txResp)
s.Require().NoError(err)
s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), txResp))
s.Assert().Equal(tc.expectedParams, txResp.Params)
})
}
Expand Down Expand Up @@ -579,7 +583,7 @@ func (s IntegrationTestSuite) TestX_CmdAddOracleProposalAndVote() {
"description": "%v",
"oracles": ["%v"],
"pairs": ["%v", "%v"]
}
}
`, proposal.Title, proposal.Description, proposal.Oracles[0],
proposal.Pairs[0], proposal.Pairs[1],
)
Expand Down
22 changes: 17 additions & 5 deletions x/pricefeed/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,22 @@ func (k Keeper) QueryPrice(goCtx context.Context, req *types.QueryPriceRequest)

tokens := common.DenomsFromPoolName(req.PairId)
token0, token1 := tokens[0], tokens[1]
currentPrice, sdkErr := k.GetCurrentPrice(ctx, token0, token1)
if sdkErr != nil {
return nil, sdkErr
currentPrice, err := k.GetCurrentPrice(ctx, token0, token1)
if err != nil {
return nil, err
}

twap, err := k.GetCurrentTWAP(ctx, token0, token1)
if err != nil {
return nil, err
}

return &types.QueryPriceResponse{
Price: types.CurrentPriceResponse{PairID: req.PairId, Price: currentPrice.Price},
Price: types.CurrentPriceResponse{
PairID: req.PairId,
Price: currentPrice.Price,
Twap: twap,
},
}, nil
}

Expand Down Expand Up @@ -81,7 +90,10 @@ func (k Keeper) QueryPrices(goCtx context.Context, req *types.QueryPricesRequest
var currentPrices types.CurrentPriceResponses
for _, currentPrice := range k.GetCurrentPrices(ctx) {
if currentPrice.PairID != "" {
currentPrices = append(currentPrices, types.CurrentPriceResponse(currentPrice))
currentPrices = append(currentPrices, types.CurrentPriceResponse{
PairID: currentPrice.PairID,
Price: currentPrice.Price,
})
}
}

Expand Down
40 changes: 40 additions & 0 deletions x/pricefeed/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package keeper_test

import (
"testing"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -107,3 +108,42 @@ func TestMarketsQuery(t *testing.T) {
assert.EqualValues(t, wantMarket, queryResp.Markets[idx])
}
}

func TestQueryPrice(t *testing.T) {
pair := common.MustNewAssetPair("ubtc:uusd")
keeper, ctx := testutilkeeper.PricefeedKeeper(t)
keeper.SetParams(ctx, types.Params{
Pairs: common.AssetPairs{pair},
TwapLookbackWindow: time.Minute * 15,
})

oracle := sample.AccAddress()
keeper.WhitelistOraclesForPairs(ctx, []sdk.AccAddress{oracle}, []common.AssetPair{pair})

// first block
_, err := keeper.PostRawPrice(ctx, oracle, pair.String(), sdk.NewDec(20_000), time.Now().Add(time.Hour))
require.NoError(t, err)
err = keeper.GatherRawPrices(ctx, "ubtc", "uusd")
require.NoError(t, err)

// second block
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 5)).WithBlockHeight(1)
_, err = keeper.PostRawPrice(ctx, oracle, "ubtc:uusd", sdk.NewDec(30_000), time.Now().Add(time.Hour))
require.NoError(t, err)
err = keeper.GatherRawPrices(ctx, "ubtc", "uusd")
require.NoError(t, err)

ctx = ctx.WithBlockTime(ctx.BlockTime().Add(time.Second * 5)).WithBlockHeight(2)
resp, err := keeper.QueryPrice(sdk.WrapSDKContext(ctx), &types.QueryPriceRequest{
PairId: "ubtc:uusd",
})

assert.Nil(t, err)
assert.Equal(t, types.QueryPriceResponse{
Price: types.CurrentPriceResponse{
PairID: "ubtc:uusd",
Price: sdk.NewDec(30_000),
Twap: sdk.NewDec(25_000),
},
}, *resp)
}
55 changes: 26 additions & 29 deletions x/pricefeed/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,27 +80,25 @@ func (k Keeper) PostRawPrice(
}

if !k.IsWhitelistedOracle(ctx, pair.String(), oracle) {
return types.PostedPrice{}, fmt.Errorf(
"oracle %s cannot post on pair %v", oracle, pair.String())
return types.PostedPrice{}, fmt.Errorf("oracle %s cannot post on pair %v", oracle, pair.String())
}

newPostedPrice := types.NewPostedPrice(pair, oracle, price, expiry)

// Emit an event containing the oracle's new price

err = ctx.EventManager().EmitTypedEvent(&types.EventOracleUpdatePrice{
if err = ctx.EventManager().EmitTypedEvent(&types.EventOracleUpdatePrice{
PairId: pair.String(),
Oracle: oracle.String(),
PairPrice: price,
Expiry: expiry,
})
if err != nil {
}); err != nil {
panic(err)
}

// Sets the raw price for a single oracle instead of an array of all oracle's raw prices
store := ctx.KVStore(k.storeKey)
store.Set(types.RawPriceKey(pair.String(), oracle), k.cdc.MustMarshal(&newPostedPrice))
newPostedPrice := types.NewPostedPrice(pair, oracle, price, expiry)
ctx.KVStore(k.storeKey).Set(
types.RawPriceKey(pair.String(), oracle),
k.cdc.MustMarshal(&newPostedPrice),
)
return newPostedPrice, nil
}

Expand Down Expand Up @@ -129,19 +127,15 @@ func (k Keeper) GatherRawPrices(ctx sdk.Context, token0 string, token1 string) e
validPrevPrice = false
}

postedPrices := k.GetRawPrices(ctx, pairID)

var notExpiredPrices []types.CurrentPrice
var unexpiredPrices []types.CurrentPrice
// filter out expired prices
for _, post := range postedPrices {
if post.Expiry.After(ctx.BlockTime()) {
notExpiredPrices = append(
notExpiredPrices,
types.NewCurrentPrice(token0, token1, post.Price))
for _, rawPrice := range k.GetRawPrices(ctx, pairID) {
if rawPrice.Expiry.After(ctx.BlockTime()) {
unexpiredPrices = append(unexpiredPrices, types.NewCurrentPrice(token0, token1, rawPrice.Price))
}
}

if len(notExpiredPrices) == 0 {
if len(unexpiredPrices) == 0 {
// NOTE: The current price stored will continue storing the most recent (expired)
// price if this is not set.
// This zero's out the current price stored value for that market and ensures
Expand All @@ -150,7 +144,7 @@ func (k Keeper) GatherRawPrices(ctx sdk.Context, token0 string, token1 string) e
return types.ErrNoValidPrice
}

medianPrice := k.CalculateMedianPrice(notExpiredPrices)
medianPrice := k.CalculateMedianPrice(unexpiredPrices)

// check case that market price was not set in genesis
if validPrevPrice && !medianPrice.Equal(prevPrice.Price) {
Expand Down Expand Up @@ -274,13 +268,16 @@ func (k Keeper) GetCurrentTWAP(ctx sdk.Context, token0 string, token1 string,
if err := assetPair.Validate(); err != nil {
return sdk.Dec{}, err
}
givenIsActive := k.IsActivePair(ctx, assetPair.String())
inverseIsActive := k.IsActivePair(ctx, assetPair.Inverse().String())
if !givenIsActive && inverseIsActive {

// invert the asset pair if the given is not existent
inverseIsActive := false
if !k.IsActivePair(ctx, assetPair.String()) && k.IsActivePair(ctx, assetPair.Inverse().String()) {
assetPair = assetPair.Inverse()
inverseIsActive = true
}
lookbackWindow := k.GetParams(ctx).TwapLookbackWindow

// earliest timestamp we'll look back until
lookbackWindow := k.GetParams(ctx).TwapLookbackWindow
lowerLimitTimestampMs := ctx.BlockTime().Add(-lookbackWindow).UnixMilli()

var cumulativePrice sdk.Dec = sdk.ZeroDec()
Expand All @@ -289,15 +286,15 @@ func (k Keeper) GetCurrentTWAP(ctx sdk.Context, token0 string, token1 string,

// traverse snapshots in reverse order
startKey := types.PriceSnapshotKey(assetPair.String(), ctx.BlockHeight())
var numShapshots int64 = 0
var numSnapshots int64 = 0
var snapshotPriceBuffer []sdk.Dec // contains snapshots at time 0
k.IteratePriceSnapshotsFrom(
/*ctx=*/ ctx,
/*start=*/ startKey,
/*end=*/ nil,
/*reverse=*/ true,
/*do=*/ func(ps *types.PriceSnapshot) (stop bool) {
numShapshots += 1
numSnapshots += 1
var timeElapsedMs int64
if ps.TimestampMs <= lowerLimitTimestampMs {
// current snapshot is below the lower limit
Expand All @@ -323,13 +320,13 @@ func (k Keeper) GetCurrentTWAP(ctx sdk.Context, token0 string, token1 string,
switch {
case cumulativePeriodMs < 0:
return sdk.Dec{}, fmt.Errorf("cumulativePeriodMs, %v, should never be negative", cumulativePeriodMs)
case (cumulativePeriodMs == 0) && (numShapshots > 0):
case (cumulativePeriodMs == 0) && (numSnapshots > 0):
sum := sdk.ZeroDec()
for _, price := range snapshotPriceBuffer {
sum = sum.Add(price)
}
return sum.QuoInt64(numShapshots), nil
case (cumulativePeriodMs == 0) && (numShapshots == 0):
return sum.QuoInt64(numSnapshots), nil
case (cumulativePeriodMs == 0) && (numSnapshots == 0):
return sdk.Dec{}, fmt.Errorf(`
failed to calculate twap, no time passed and no snapshots have been taken since
ctx.BlockTime: %v,
Expand Down
Loading