Skip to content

Commit

Permalink
improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
BrendanChou committed May 21, 2024
1 parent 564a5ac commit a10ee8d
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 118 deletions.
72 changes: 0 additions & 72 deletions protocol/lib/quantums.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,75 +93,3 @@ func QuoteToBaseQuantums(
}
return result
}

// multiplyByPrice multiples a value by price, factoring in exponents of base
// and quote currencies.
// Given `value`, returns result of the following:
//
// `value * priceValue * 10^(priceExponent + baseAtomicResolution - quoteAtomicResolution)` [expression 2]
//
// Note that both `BaseToQuoteQuantums` and `FundingRateToIndex` directly wrap around this function.
// - For `BaseToQuoteQuantums`, substituing `value` with `baseQuantums` in expression 2 yields expression 1.
// - For `FundingRateToIndex`, substituing `value` with `fundingRatePpm * time` in expression 2 yields expression 3.
func multiplyByPrice(
value *big.Rat,
baseCurrencyAtomicResolution int32,
priceValue uint64,
priceExponent int32,
) (result *big.Int) {
ratResult := new(big.Rat).SetUint64(priceValue)

ratResult.Mul(
ratResult,
value,
)

ratResult.Mul(
ratResult,
RatPow10(priceExponent+baseCurrencyAtomicResolution-QuoteCurrencyAtomicResolution),
)

return new(big.Int).Quo(
ratResult.Num(),
ratResult.Denom(),
)
}

// FundingRateToIndex converts funding rate (in ppm) to FundingIndex given the oracle price.
//
// To get funding index from funding rate, we know that:
//
// - `fundingPaymentQuoteQuantum = fundingRatePpm / 1_000_000 * time * quoteQuantums`
// - Divide both sides by `baseQuantums`:
// - Left side: `fundingPaymentQuoteQuantums / baseQuantums = fundingIndexDelta / 1_000_000`
// - right side:
// ```
// fundingRate * time * quoteQuantums / baseQuantums = fundingRatePpm / 1_000_000 *
// priceValue * 10^(priceExponent + baseCurrencyAtomicResolution - quoteCurrencyAtomicResolution) [expression 3]
// ```
//
// Hence, further multiplying both sides by 1_000_000, we have:
//
// fundingIndexDelta =
// (fundingRatePpm * time) * priceValue *
// 10^(priceExponent + baseCurrencyAtomicResolution - quoteCurrencyAtomicResolution)
//
// Arguments:
//
// proratedFundingRate: prorated funding rate adjusted by time delta, in parts-per-million
// baseCurrencyAtomicResolution: atomic resolution of the base currency
// priceValue: index price of the perpetual market according to the pricesKeeper
// priceExponent: priceExponent of the market according to the pricesKeeper
func FundingRateToIndex(
proratedFundingRate *big.Rat,
baseCurrencyAtomicResolution int32,
priceValue uint64,
priceExponent int32,
) (fundingIndex *big.Int) {
return multiplyByPrice(
proratedFundingRate,
baseCurrencyAtomicResolution,
priceValue,
priceExponent,
)
}
6 changes: 6 additions & 0 deletions protocol/testutil/perpetuals/perpetuals.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ func WithLiquidityTier(liquidityTier uint32) PerpetualModifierOption {
}
}

func WithAtomicResolution(atomicResolution int32) PerpetualModifierOption {
return func(cp *perptypes.Perpetual) {
cp.Params.AtomicResolution = atomicResolution
}
}

func WithMarketType(marketType perptypes.PerpetualMarketType) PerpetualModifierOption {
return func(cp *perptypes.Perpetual) {
cp.Params.MarketType = marketType
Expand Down
47 changes: 47 additions & 0 deletions protocol/x/perpetuals/funding/funding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package funding

import (
"math/big"

"github.com/dydxprotocol/v4-chain/protocol/lib"
"github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types"
pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types"
)

// GetFundingIndexDelta returns `fundingIndexDelta` which represents the change of the funding index
// given the funding rate, the time since the last funding tick, and the oracle price. The index delta
// is in parts-per-million (PPM) and is calculated as follows:
//
// indexDelta =
// fundingRatePpm *
// (time / realizationPeriod) *
// quoteQuantumsPerBaseQuantum
//
// Any multiplication is done before division to avoid precision loss.
func GetFundingIndexDelta(
perp types.Perpetual,
marketPrice pricestypes.MarketPrice,
big8hrFundingRatePpm *big.Int,
timeSinceLastFunding uint32,
) (fundingIndexDelta *big.Int) {
// Get pro-rated funding rate adjusted by time delta.
result := new(big.Int).SetUint64(uint64(timeSinceLastFunding))

// Multiply by the time-delta numerator upfront.
result.Mul(result, big8hrFundingRatePpm)

// Multiply by the price of the asset.
result = lib.BaseToQuoteQuantums(
result,
perp.Params.AtomicResolution,
marketPrice.Price,
marketPrice.Exponent,
)

// Divide by the time-delta denominator.
// Use truncated division (towards zero) instead of Euclidean division.
// TODO(DEC-1536): Make the 8-hour funding rate period configurable.
result.Quo(result, big.NewInt(60*60*8))

return result
}
89 changes: 89 additions & 0 deletions protocol/x/perpetuals/funding/funding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package funding_test

import (
"math/big"
"testing"

"github.com/stretchr/testify/require"

perptest "github.com/dydxprotocol/v4-chain/protocol/testutil/perpetuals"
"github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/funding"
"github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types"
pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types"
)

func TestGetFundingIndexDelta(t *testing.T) {
testCases := map[string]struct {
perp types.Perpetual
marketPrice pricestypes.MarketPrice
big8hrFundingRatePpm *big.Int
timeSinceLastFunding uint32
expected *big.Int
}{
"Positive Funding Rate (rounds towards zero)": {
perp: *perptest.GeneratePerpetual(
perptest.WithAtomicResolution(-12),
),
marketPrice: pricestypes.MarketPrice{
Id: 0,
Exponent: 0,
Price: 1_000,
},
big8hrFundingRatePpm: big.NewInt(1_001_999),
timeSinceLastFunding: 8 * 60 * 60,
expected: big.NewInt(1_001),
},
"Negative Funding Rate (rounds towards zero)": {
perp: *perptest.GeneratePerpetual(
perptest.WithAtomicResolution(-12),
),
marketPrice: pricestypes.MarketPrice{
Id: 0,
Exponent: 0,
Price: 1_000,
},
big8hrFundingRatePpm: big.NewInt(-1_001_999),
timeSinceLastFunding: 8 * 60 * 60,
expected: big.NewInt(-1_001),
},
"Varied parameters (1)": {
perp: *perptest.GeneratePerpetual(
perptest.WithAtomicResolution(-4),
),
marketPrice: pricestypes.MarketPrice{
Id: 0,
Exponent: 2,
Price: 1_000,
},
big8hrFundingRatePpm: big.NewInt(-1_001_999),
timeSinceLastFunding: 8 * 60 * 60 / 2,
expected: big.NewInt(-5_009_995_000_000),
},
"Varied parameters (2)": {
perp: *perptest.GeneratePerpetual(
perptest.WithAtomicResolution(0),
),
marketPrice: pricestypes.MarketPrice{
Id: 0,
Exponent: -6,
Price: 1_000,
},
big8hrFundingRatePpm: big.NewInt(-1_001_999),
timeSinceLastFunding: 8 * 60 * 60 / 8,
expected: big.NewInt(-125_249_875),
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
result := funding.GetFundingIndexDelta(
tc.perp,
tc.marketPrice,
tc.big8hrFundingRatePpm,
tc.timeSinceLastFunding,
)

require.Equal(t, tc.expected, result)
})
}
}
59 changes: 13 additions & 46 deletions protocol/x/perpetuals/keeper/perpetual.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/dydxprotocol/v4-chain/protocol/lib/log"
"github.com/dydxprotocol/v4-chain/protocol/lib/metrics"
epochstypes "github.com/dydxprotocol/v4-chain/protocol/x/epochs/types"
"github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/funding"
"github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types"
pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types"
gometrics "github.com/hashicorp/go-metrics"
Expand Down Expand Up @@ -432,46 +433,6 @@ func (k Keeper) MaybeProcessNewFundingSampleEpoch(
k.processPremiumVotesIntoSamples(ctx, newFundingSampleEpoch)
}

// getFundingIndexDelta returns fundingIndexDelta which represents the change of FundingIndex since
// the last time `funding-tick` was processed.
// TODO(DEC-1536): Make the 8-hour funding rate period configurable.
func (k Keeper) getFundingIndexDelta(
ctx sdk.Context,
perp types.Perpetual,
big8hrFundingRatePpm *big.Int,
timeSinceLastFunding uint32,
) (
fundingIndexDelta *big.Int,
err error,
) {
marketPrice, err := k.pricesKeeper.GetMarketPrice(ctx, perp.Params.MarketId)
if err != nil {
return nil, fmt.Errorf("failed to get market price for perpetual %v, err = %w", perp.Params.Id, err)
}

// Get pro-rated funding rate adjusted by time delta.
proratedFundingRate := new(big.Rat).SetInt(big8hrFundingRatePpm)
proratedFundingRate.Mul(
proratedFundingRate,
new(big.Rat).SetUint64(uint64(timeSinceLastFunding)),
)

proratedFundingRate.Quo(
proratedFundingRate,
// TODO(DEC-1536): Make the 8-hour funding rate period configurable.
new(big.Rat).SetUint64(3600*8),
)

bigFundingIndexDelta := lib.FundingRateToIndex(
proratedFundingRate,
perp.Params.AtomicResolution,
marketPrice.Price,
marketPrice.Exponent,
)

return bigFundingIndexDelta, nil
}

// GetAddPremiumVotes returns the newest premiums for all perpetuals,
// if the current block is the start of a new funding-sample epoch.
// Otherwise, does nothing and returns an empty message.
Expand Down Expand Up @@ -770,20 +731,26 @@ func (k Keeper) MaybeProcessNewFundingTickEpoch(ctx sdk.Context) {
))
}

// Update the funding index if the funding rate is non-zero.
if bigFundingRatePpm.Sign() != 0 {
fundingIndexDelta, err := k.getFundingIndexDelta(
ctx,
// Get the price of the perpetual from state.
marketPrice, err := k.pricesKeeper.GetMarketPrice(ctx, perp.Params.MarketId)
if err != nil {
panic(err)
}

// Calculate the delta in the funding index.
fundingIndexDelta := funding.GetFundingIndexDelta(
perp,
marketPrice,
bigFundingRatePpm,
// use funding-tick duration as `timeSinceLastFunding`
// TODO(DEC-1483): Handle the case when duration value is updated
// during the epoch.
fundingTickEpochInfo.Duration,
)
if err != nil {
panic(err)
}

// Update the funding index in state.
if err := k.ModifyFundingIndex(ctx, perp.Params.Id, fundingIndexDelta); err != nil {
panic(err)
}
Expand Down Expand Up @@ -1095,7 +1062,7 @@ func GetSettlementPpmWithPerpetual(

bigNetSettlementPpm = new(big.Int).Mul(indexDelta, quantums)

// `bigNetSettlementPpm` carries sign. `indexDelta`` is the increase in `fundingIndex`, so if
// `bigNetSettlementPpm` carries sign. `indexDelta` is the increase in `fundingIndex`, so if
// the position is long (positive), the net settlement should be short (negative), and vice versa.
// Thus, always negate `bigNetSettlementPpm` here.
bigNetSettlementPpm = bigNetSettlementPpm.Neg(bigNetSettlementPpm)
Expand Down

0 comments on commit a10ee8d

Please sign in to comment.