From edcc82b79e7298e2bd79da8bdde52cfca488743d Mon Sep 17 00:00:00 2001 From: Brendan Chou <3680392+BrendanChou@users.noreply.github.com> Date: Thu, 13 Jun 2024 11:37:08 -0400 Subject: [PATCH] [Performance] Remove the need to get each perpetual and price twice when checking collateralization (#1681) --- protocol/mocks/PerpetualsKeeper.go | 39 -- protocol/x/clob/keeper/deleveraging.go | 15 +- protocol/x/clob/keeper/liquidations.go | 67 ++-- protocol/x/clob/types/expected_keepers.go | 18 +- protocol/x/perpetuals/keeper/perpetual.go | 84 ++-- .../x/perpetuals/keeper/perpetual_test.go | 331 +-------------- protocol/x/perpetuals/lib/lib.go | 26 ++ protocol/x/perpetuals/lib/lib_test.go | 378 ++++++++++++++++++ protocol/x/perpetuals/types/types.go | 9 - protocol/x/subaccounts/keeper/subaccount.go | 46 +-- .../x/subaccounts/types/expected_keepers.go | 32 +- 11 files changed, 525 insertions(+), 520 deletions(-) diff --git a/protocol/mocks/PerpetualsKeeper.go b/protocol/mocks/PerpetualsKeeper.go index f7f1cee2aa..517cb7d824 100644 --- a/protocol/mocks/PerpetualsKeeper.go +++ b/protocol/mocks/PerpetualsKeeper.go @@ -122,45 +122,6 @@ func (_m *PerpetualsKeeper) GetAllPerpetuals(ctx types.Context) []perpetualstype return r0 } -// GetMarginRequirements provides a mock function with given fields: ctx, id, bigQuantums -func (_m *PerpetualsKeeper) GetMarginRequirements(ctx types.Context, id uint32, bigQuantums *big.Int) (*big.Int, *big.Int, error) { - ret := _m.Called(ctx, id, bigQuantums) - - if len(ret) == 0 { - panic("no return value specified for GetMarginRequirements") - } - - var r0 *big.Int - var r1 *big.Int - var r2 error - if rf, ok := ret.Get(0).(func(types.Context, uint32, *big.Int) (*big.Int, *big.Int, error)); ok { - return rf(ctx, id, bigQuantums) - } - if rf, ok := ret.Get(0).(func(types.Context, uint32, *big.Int) *big.Int); ok { - r0 = rf(ctx, id, bigQuantums) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*big.Int) - } - } - - if rf, ok := ret.Get(1).(func(types.Context, uint32, *big.Int) *big.Int); ok { - r1 = rf(ctx, id, bigQuantums) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*big.Int) - } - } - - if rf, ok := ret.Get(2).(func(types.Context, uint32, *big.Int) error); ok { - r2 = rf(ctx, id, bigQuantums) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - // GetNetCollateral provides a mock function with given fields: ctx, id, bigQuantums func (_m *PerpetualsKeeper) GetNetCollateral(ctx types.Context, id uint32, bigQuantums *big.Int) (*big.Int, error) { ret := _m.Called(ctx, id, bigQuantums) diff --git a/protocol/x/clob/keeper/deleveraging.go b/protocol/x/clob/keeper/deleveraging.go index 49d5ec84f4..3ba9bc4870 100644 --- a/protocol/x/clob/keeper/deleveraging.go +++ b/protocol/x/clob/keeper/deleveraging.go @@ -6,8 +6,6 @@ import ( "math/big" "time" - perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" - errorsmod "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/telemetry" @@ -19,6 +17,8 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" assettypes "github.com/dydxprotocol/v4-chain/protocol/x/assets/types" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + perplib "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/lib" + perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) @@ -592,11 +592,12 @@ func (k Keeper) ProcessDeleveraging( } // Stat quantums deleveraged in quote quantums. - if deleveragedQuoteQuantums, err := k.perpetualsKeeper.GetNetCollateral( - ctx, - perpetualId, - new(big.Int).Abs(deltaBaseQuantums), - ); err == nil { + if perpetual, marketPrice, err := k.perpetualsKeeper.GetPerpetualAndMarketPrice(ctx, perpetualId); err == nil { + deleveragedQuoteQuantums := perplib.GetNetNotionalInQuoteQuantums( + perpetual, + marketPrice, + new(big.Int).Abs(deltaBaseQuantums), + ) labels := []metrics.Label{ metrics.GetLabelForIntValue(metrics.PerpetualId, int(perpetualId)), metrics.GetLabelForBoolValue(metrics.CheckTx, ctx.IsCheckTx()), diff --git a/protocol/x/clob/keeper/liquidations.go b/protocol/x/clob/keeper/liquidations.go index 7f89c5b9a4..838b7c6f51 100644 --- a/protocol/x/clob/keeper/liquidations.go +++ b/protocol/x/clob/keeper/liquidations.go @@ -16,6 +16,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/lib/log" "github.com/dydxprotocol/v4-chain/protocol/lib/metrics" "github.com/dydxprotocol/v4-chain/protocol/x/clob/types" + perplib "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/lib" satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types" ) @@ -455,51 +456,36 @@ func (k Keeper) GetBankruptcyPriceInQuoteQuantums( ) } + perpetual, + marketPrice, + liquidityTier, + err := k.perpetualsKeeper. + GetPerpetualAndMarketPriceAndLiquidityTier(ctx, perpetualId) + if err != nil { + return nil, err + } + // `DNNV = PNNVAD - PNNV`, where `PNNVAD` is the perpetual's net notional // with a position size of `PS + deltaQuantums`. // Note that we are intentionally not calculating `DNNV` from `deltaQuantums` // directly to avoid rounding errors. - pnnvBig, err := k.perpetualsKeeper.GetNetNotional( - ctx, - perpetualId, + pnnvBig, _, pmmrBig := perplib.GetNetCollateralAndMarginRequirements( + perpetual, + marketPrice, + liquidityTier, psBig, ) - if err != nil { - return nil, err - } - - pnnvadBig, err := k.perpetualsKeeper.GetNetNotional( - ctx, - perpetualId, + pnnvadBig, _, pmmradBig := perplib.GetNetCollateralAndMarginRequirements( + perpetual, + marketPrice, + liquidityTier, new(big.Int).Add(psBig, deltaQuantums), ) - if err != nil { - return nil, err - } - - dnnvBig := new(big.Int).Sub(pnnvadBig, pnnvBig) - // `DMMR = PMMRAD - PMMR`, where `PMMRAD` is the perpetual's maintenance margin requirement // with a position size of `PS + deltaQuantums`. // Note that we cannot directly calculate `DMMR` from `deltaQuantums` because the maintenance // margin requirement function could be non-linear. - _, pmmrBig, err := k.perpetualsKeeper.GetMarginRequirements(ctx, perpetualId, psBig) - if err != nil { - return nil, err - } - - _, pmmradBig, err := k.perpetualsKeeper.GetMarginRequirements( - ctx, - perpetualId, - new(big.Int).Add( - psBig, - deltaQuantums, - ), - ) - if err != nil { - return nil, err - } - + dnnvBig := new(big.Int).Sub(pnnvadBig, pnnvBig) dmmrBig := new(big.Int).Sub(pmmradBig, pmmrBig) // `dmmrBig` should never be positive if `| PS | >= | PS + deltaQuantums |`. If it is, panic. if dmmrBig.Sign() == 1 { @@ -565,15 +551,20 @@ func (k Keeper) GetFillablePrice( ) } - pnnvBig, err := k.perpetualsKeeper.GetNetCollateral(ctx, perpetualId, psBig) + perpetual, + marketPrice, + liquidityTier, + err := k.perpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier(ctx, perpetualId) if err != nil { return nil, err } - _, pmmrBig, err := k.perpetualsKeeper.GetMarginRequirements(ctx, perpetualId, psBig) - if err != nil { - return nil, err - } + pnnvBig, _, pmmrBig := perplib.GetNetCollateralAndMarginRequirements( + perpetual, + marketPrice, + liquidityTier, + psBig, + ) tncBig, _, diff --git a/protocol/x/clob/types/expected_keepers.go b/protocol/x/clob/types/expected_keepers.go index a79e87f4a4..755aa4fd20 100644 --- a/protocol/x/clob/types/expected_keepers.go +++ b/protocol/x/clob/types/expected_keepers.go @@ -105,21 +105,13 @@ type PerpetualsKeeper interface { bigBaseQuantums *big.Int, err error, ) - GetNetCollateral( + GetPerpetualAndMarketPriceAndLiquidityTier( ctx sdk.Context, - id uint32, - bigQuantums *big.Int, - ) ( - bigNetCollateralQuoteQuantums *big.Int, - err error, - ) - GetMarginRequirements( - ctx sdk.Context, - id uint32, - bigQuantums *big.Int, + perpetualId uint32, ) ( - bigInitialMarginQuoteQuantums *big.Int, - bigMaintenanceMarginQuoteQuantums *big.Int, + perpetual perpetualsmoduletypes.Perpetual, + price pricestypes.MarketPrice, + liquidityTier perpetualsmoduletypes.LiquidityTier, err error, ) GetPerpetual( diff --git a/protocol/x/perpetuals/keeper/perpetual.go b/protocol/x/perpetuals/keeper/perpetual.go index 08a9cc7eba..e15d7aa380 100644 --- a/protocol/x/perpetuals/keeper/perpetual.go +++ b/protocol/x/perpetuals/keeper/perpetual.go @@ -874,64 +874,6 @@ func (k Keeper) GetNetCollateral( return k.GetNetNotional(ctx, id, bigQuantums) } -// GetMarginRequirements returns initial and maintenance margin requirements in quote quantums, given the position -// size in base quantums. -// -// Margin requirements are a function of the absolute value of the open notional of the position as well as -// the parameters of the relevant `LiquidityTier` of the perpetual. -// Initial margin requirement is determined by multiplying `InitialMarginPpm` and `notionalValue`. -// `notionalValue` is determined by multiplying the size of the position by the oracle price of the position. -// Maintenance margin requirement is then simply a fraction (`maintenanceFractionPpm`) of initial margin requirement. -// -// Returns an error if a perpetual with `id`, `perpetual.Params.MarketId`, or -// `perpetual.Params.LiquidityTier` does not exist. -// -// Note that this function is getting called very frequently; metrics in this function -// should be sampled to reduce CPU time. -func (k Keeper) GetMarginRequirements( - ctx sdk.Context, - id uint32, - bigQuantums *big.Int, -) ( - bigInitialMarginQuoteQuantums *big.Int, - bigMaintenanceMarginQuoteQuantums *big.Int, - err error, -) { - if rand.Float64() < metrics.LatencyMetricSampleRate { - defer metrics.ModuleMeasureSinceWithLabels( - types.ModuleName, - []string{metrics.GetMarginRequirements, metrics.Latency}, - time.Now(), - []gometrics.Label{ - metrics.GetLabelForStringValue( - metrics.SampleRate, - fmt.Sprintf("%f", metrics.LatencyMetricSampleRate), - ), - }, - ) - } - - // Get perpetual and market price. - perpetual, marketPrice, err := k.GetPerpetualAndMarketPrice(ctx, id) - if err != nil { - return nil, nil, err - } - // Get perpetual's liquidity tier. - liquidityTier, err := k.GetLiquidityTier(ctx, perpetual.Params.LiquidityTier) - if err != nil { - return nil, nil, err - } - - bigInitialMarginQuoteQuantums, - bigMaintenanceMarginQuoteQuantums = perplib.GetMarginRequirementsInQuoteQuantums( - perpetual, - marketPrice, - liquidityTier, - bigQuantums, - ) - return bigInitialMarginQuoteQuantums, bigMaintenanceMarginQuoteQuantums, nil -} - // GetSettlementPpm returns the net settlement amount ppm (in quote quantums) given // the perpetual Id and position size (in base quantums). // When handling rounding, always round positive settlement amount to zero, and @@ -1244,6 +1186,32 @@ func (k Keeper) GetPerpetualAndMarketPrice( return perpetual, marketPrice, nil } +// GetPerpetualAndMarketPriceAndLiquidityTier retrieves a Perpetual by its id, its corresponding MarketPrice, +// and its corresponding LiquidityTier. +func (k Keeper) GetPerpetualAndMarketPriceAndLiquidityTier( + ctx sdk.Context, + perpetualId uint32, +) ( + types.Perpetual, + pricestypes.MarketPrice, + types.LiquidityTier, + error, +) { + perpetual, err := k.GetPerpetual(ctx, perpetualId) + if err != nil { + return perpetual, pricestypes.MarketPrice{}, types.LiquidityTier{}, err + } + marketPrice, err := k.pricesKeeper.GetMarketPrice(ctx, perpetual.Params.MarketId) + if err != nil { + return perpetual, marketPrice, types.LiquidityTier{}, err + } + liquidityTier, err := k.GetLiquidityTier(ctx, perpetual.Params.LiquidityTier) + if err != nil { + return perpetual, marketPrice, liquidityTier, err + } + return perpetual, marketPrice, liquidityTier, nil +} + // Performs the following validation (stateful and stateless) on a `Perpetual` // structs fields, returning an error if any conditions are false: // - MarketId is not a valid market. diff --git a/protocol/x/perpetuals/keeper/perpetual_test.go b/protocol/x/perpetuals/keeper/perpetual_test.go index f24929021a..a6218408dc 100644 --- a/protocol/x/perpetuals/keeper/perpetual_test.go +++ b/protocol/x/perpetuals/keeper/perpetual_test.go @@ -674,324 +674,18 @@ func TestModifyOpenInterest_Mixed(t *testing.T) { } } -func TestGetMarginRequirements_Success(t *testing.T) { - oneBip := math.Pow10(2) - tests := map[string]struct { - price uint64 - exponent int32 - baseCurrencyAtomicResolution int32 - bigBaseQuantums *big.Int - initialMarginPpm uint32 - maintenanceFractionPpm uint32 - openInterest *big.Int - openInterestLowerCap uint64 - openInterestUpperCap uint64 - bigExpectedInitialMargin *big.Int - bigExpectedMaintenanceMargin *big.Int - }{ - "InitialMargin 2 BIPs, MaintenanceMargin 1 BIP, positive exponent, atomic resolution 8": { - price: 5_555, - exponent: 2, - baseCurrencyAtomicResolution: -8, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 2), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - bigExpectedInitialMargin: big.NewInt(7_777), - bigExpectedMaintenanceMargin: big.NewInt(3_889), - }, - "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, atomic resolution 4": { - price: 5_555, - exponent: 0, - baseCurrencyAtomicResolution: -4, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 100), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - bigExpectedInitialMargin: big.NewInt(38_885_000), - bigExpectedMaintenanceMargin: big.NewInt(19_442_500), - }, - "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, positive exponent, atomic resolution 0": { - price: 42, - exponent: 5, - baseCurrencyAtomicResolution: -0, - bigBaseQuantums: big.NewInt(88), - initialMarginPpm: uint32(oneBip * 100), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - bigExpectedInitialMargin: big.NewInt(3_696_000_000_000), - bigExpectedMaintenanceMargin: big.NewInt(1_848_000_000_000), - }, - "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, negative exponent, atomic resolution 6": { - price: 42_000_000, - exponent: -2, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(-5_000), - initialMarginPpm: uint32(oneBip * 100), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - bigExpectedInitialMargin: big.NewInt(21_000_000), - bigExpectedMaintenanceMargin: big.NewInt(10_500_000), - }, - "InitialMargin 10_000 BIPs (max), MaintenanceMargin 10_000 BIPs (max), atomic resolution 6": { - price: 5_555, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 10_000), - maintenanceFractionPpm: uint32(1_000_000), // 100% of IM - bigExpectedInitialMargin: big.NewInt(38_885_000), - bigExpectedMaintenanceMargin: big.NewInt(38_885_000), - }, - "InitialMargin 100 BIPs, MaintenanceMargin 100 BIPs, atomic resolution 6": { - price: 5_555, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 100), - maintenanceFractionPpm: uint32(1_000_000), // 100% of IM - bigExpectedInitialMargin: big.NewInt(388_850), - bigExpectedMaintenanceMargin: big.NewInt(388_850), - }, - "InitialMargin 0.02 BIPs, MaintenanceMargin 0.01 BIPs, positive exponent, atomic resolution 6": { - price: 5_555, - exponent: 3, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(-7_000), - initialMarginPpm: uint32(oneBip * 0.02), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - bigExpectedInitialMargin: big.NewInt(77_770), - bigExpectedMaintenanceMargin: big.NewInt(38_885), - }, - "InitialMargin 0 BIPs (min), MaintenanceMargin 0 BIPs (min), atomic resolution 6": { - price: 5_555, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 0), - maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, - bigExpectedInitialMargin: big.NewInt(0), - bigExpectedMaintenanceMargin: big.NewInt(0), - }, - "Price is zero, atomic resolution 6": { - price: 0, - exponent: 1, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(-7_000), - initialMarginPpm: uint32(oneBip * 1), - maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, - bigExpectedInitialMargin: big.NewInt(0), - bigExpectedMaintenanceMargin: big.NewInt(0), - }, - "Price and quantums are max uints": { - price: math.MaxUint64, - exponent: 1, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: new(big.Int).SetUint64(math.MaxUint64), - initialMarginPpm: uint32(oneBip * 1), - maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, - bigExpectedInitialMargin: big_testutil.MustFirst( - new(big.Int).SetString("340282366920938463426481119284349109", 10), - ), - bigExpectedMaintenanceMargin: big_testutil.MustFirst( - new(big.Int).SetString("340282366920938463426481119284349109", 10), - ), - }, - "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, atomic resolution 6": { - price: 5_555, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(7_000), - initialMarginPpm: uint32(oneBip * 100), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 - // = 10_000 * 38_885_000 / 1_000_000 ~= 388_850. - bigExpectedInitialMargin: big.NewInt(388_850), - bigExpectedMaintenanceMargin: big.NewInt(388_850 / 2), - }, - "InitialMargin 20%, MaintenanceMargin 10%, atomic resolution 6": { - price: 36_750, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(12_000), - initialMarginPpm: uint32(200_000), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - // quoteQuantums = 36_750 * 12_000 = 441_000_000 - // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 - // = 200_000 * 441_000_000 / 1_000_000 ~= 88_200_000 - bigExpectedInitialMargin: big.NewInt(88_200_000), - bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), - }, - "InitialMargin 5%, MaintenanceMargin 3%, atomic resolution 6": { - price: 123_456, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(74_523), - initialMarginPpm: uint32(50_000), - maintenanceFractionPpm: uint32(600_000), // 60% of IM - // quoteQuantums = 123_456 * 74_523 = 9_200_311_488 - // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 - // = 50_000 * 9_200_311_488 / 1_000_000 ~= 460_015_575 - bigExpectedInitialMargin: big.NewInt(460_015_575), - bigExpectedMaintenanceMargin: big.NewInt(276_009_345), - }, - "InitialMargin 25%, MaintenanceMargin 15%, atomic resolution 6": { - price: 123_456, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(74_523), - initialMarginPpm: uint32(250_000), - maintenanceFractionPpm: uint32(600_000), // 60% of IM - // quoteQuantums = 123_456 * 74_523 = 9_200_311_488 - bigExpectedInitialMargin: big.NewInt(2_300_077_872), - bigExpectedMaintenanceMargin: big.NewInt(1_380_046_724), // Rounded up - }, - "OIMF: IM 20%, scaled to 60%, MaintenanceMargin 10%, atomic resolution 6": { - price: 36_750, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(12_000), - initialMarginPpm: uint32(200_000), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - openInterest: big.NewInt(408_163_265), // 408.163265 - openInterestLowerCap: 10_000_000_000_000, - openInterestUpperCap: 20_000_000_000_000, - // openInterestNotional = 408_163_265 * 36_750 = 14_999_999_988_750 - // percentageOfCap = (openInterestNotional - lowerCap) / (upperCap - lowerCap) = 0.499999998875 - // adjustedIMF = (0.499999998875) * 0.8 + 0.2 = 0.5999999991 (rounded is 599_999 ppm) - // bigExpectedInitialMargin = bigBaseQuantums * price * adjustedIMF = 264_599_559 - bigExpectedInitialMargin: big.NewInt(264_599_559), - bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), - }, - "OIMF: IM 20%, scaled to 100%, MaintenanceMargin 10%, atomic resolution 6": { - price: 36_750, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(12_000), - initialMarginPpm: uint32(200_000), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - openInterest: big.NewInt(1_000_000_000), // 1000 or ~$36mm notional - openInterestLowerCap: 10_000_000_000_000, - openInterestUpperCap: 20_000_000_000_000, - // quoteQuantums = 36_750 * 12_000 = 441_000_000 - // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 - // = 200_000 * 441_000_000 / 1_000_000 ~= 88_200_000 - bigExpectedInitialMargin: big.NewInt(441_000_000), - bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), - }, - "OIMF: IM 20%, lower_cap < realistic open interest < upper_cap, MaintenanceMargin 10%, atomic resolution 6": { - price: 36_750, - exponent: 0, - baseCurrencyAtomicResolution: -6, - bigBaseQuantums: big.NewInt(12_000), - initialMarginPpm: uint32(200_000), - maintenanceFractionPpm: uint32(500_000), // 50% of IM - openInterest: big.NewInt(1_123_456_789), // 1123.456 or ~$41mm notional - openInterestLowerCap: 25_000_000_000_000, - openInterestUpperCap: 50_000_000_000_000, - // openInterestNotional = 1_123_456_789 * 36_750 = 41_287_036_995_750 - // percentageOfCap = (openInterestNotional - lowerCap) / (upperCap - lowerCap) = 0.65148147983 - // adjustedIMF = (0.65148147983) * 0.8 + 0.2 = 0.721185183864 (rounded is 721_185 ppm) - // bigExpectedInitialMargin = bigBaseQuantums * price * adjustedIMF = 318_042_585 - bigExpectedInitialMargin: big.NewInt(318_042_585), - bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), - }, - } - - // Run tests. - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - // Individual test setup. - pc := keepertest.PerpetualsKeepers(t) - // Create a new market param and price. - marketId := keepertest.GetNumMarkets(t, pc.Ctx, pc.PricesKeeper) - _, err := pc.PricesKeeper.CreateMarket( - pc.Ctx, - pricestypes.MarketParam{ - Id: marketId, - Pair: "marketName", - Exponent: tc.exponent, - MinExchanges: uint32(1), - MinPriceChangePpm: uint32(50), - ExchangeConfigJson: "{}", - }, - pricestypes.MarketPrice{ - Id: marketId, - Exponent: tc.exponent, - Price: 1_000, // leave this as a placeholder b/c we cannot set the price to 0 - }, - ) - require.NoError(t, err) - - // Update `Market.price`. By updating prices this way, we can simulate conditions where the oracle - // price may become 0. - err = pc.PricesKeeper.UpdateMarketPrices( - pc.Ctx, - []*pricestypes.MsgUpdateMarketPrices_MarketPrice{pricestypes.NewMarketPriceUpdate( - marketId, - tc.price, - )}, - ) - require.NoError(t, err) - - // Create `LiquidityTier` struct. - _, err = pc.PerpetualsKeeper.SetLiquidityTier( - pc.Ctx, - 0, - "name", - tc.initialMarginPpm, - tc.maintenanceFractionPpm, - 1, // dummy impact notional value - tc.openInterestLowerCap, - tc.openInterestUpperCap, - ) - require.NoError(t, err) - - // Create `Perpetual` struct with baseAssetAtomicResolution and marketId. - perpetual, err := pc.PerpetualsKeeper.CreatePerpetual( - pc.Ctx, - 0, // PerpetualId - "getMarginRequirementsTicker", // Ticker - marketId, // MarketId - tc.baseCurrencyAtomicResolution, // AtomicResolution - int32(0), // DefaultFundingPpm - 0, // LiquidityTier - types.PerpetualMarketType_PERPETUAL_MARKET_TYPE_CROSS, // MarketType - ) - require.NoError(t, err) - - // If test case contains non-nil open interest, set it up. - if tc.openInterest != nil { - require.NoError(t, pc.PerpetualsKeeper.ModifyOpenInterest( - pc.Ctx, - perpetual.Params.Id, - tc.openInterest, // initialized as zero, so passing `openInterest` as delta amount. - )) - } - - // Verify initial and maintenance margin requirements are calculated correctly. - bigInitialMargin, bigMaintenanceMargin, err := pc.PerpetualsKeeper.GetMarginRequirements( - pc.Ctx, - perpetual.Params.Id, - tc.bigBaseQuantums, - ) - require.NoError(t, err) - - require.Equal(t, tc.bigExpectedInitialMargin, bigInitialMargin, "Initial margin mismatch") - require.Equal(t, tc.bigExpectedMaintenanceMargin, bigMaintenanceMargin, "Maintenance margin mismatch") - }) - } -} - -func TestGetMarginRequirements_PerpetualNotFound(t *testing.T) { +func TestGetPerpetualAndMarketPriceAndLiquidityTier_PerpetualNotFound(t *testing.T) { pc := keepertest.PerpetualsKeepers(t) nonExistentPerpetualId := uint32(0) - _, _, err := pc.PerpetualsKeeper.GetMarginRequirements( + _, _, _, err := pc.PerpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier( pc.Ctx, nonExistentPerpetualId, - big.NewInt(-1), ) require.EqualError(t, err, errorsmod.Wrap(types.ErrPerpetualDoesNotExist, fmt.Sprint(nonExistentPerpetualId)).Error()) require.ErrorIs(t, err, types.ErrPerpetualDoesNotExist) } -func TestGetMarginRequirements_MarketNotFound(t *testing.T) { +func TestGetPerpetualAndMarketPriceAndLiquidityTier_MarketNotFound(t *testing.T) { pc := keepertest.PerpetualsKeepers(t) // Create liquidity tiers and perpetuals, @@ -1007,22 +701,20 @@ func TestGetMarginRequirements_MarketNotFound(t *testing.T) { perpetualStore.Set(lib.Uint32ToKey(perpetual.Params.Id), b) // Getting margin requirements for perpetual with bad MarketId should return an error. - _, _, err := pc.PerpetualsKeeper.GetMarginRequirements( + _, _, _, err := pc.PerpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier( pc.Ctx, perpetual.Params.Id, - big.NewInt(-1), ) - expectedErrorStr := fmt.Sprintf( - "Market ID %d does not exist on perpetual ID %d", - perpetual.Params.MarketId, - perpetual.Params.Id, + require.EqualError( + t, + err, + errorsmod.Wrap(pricestypes.ErrMarketPriceDoesNotExist, fmt.Sprint(nonExistentMarketId)).Error(), ) - require.EqualError(t, err, errorsmod.Wrap(types.ErrMarketDoesNotExist, expectedErrorStr).Error()) - require.ErrorIs(t, err, types.ErrMarketDoesNotExist) + require.ErrorIs(t, err, pricestypes.ErrMarketPriceDoesNotExist) } -func TestGetMarginRequirements_LiquidityTierNotFound(t *testing.T) { +func TestGetPerpetualAndMarketPriceAndLiquidityTier_LiquidityTierNotFound(t *testing.T) { pc := keepertest.PerpetualsKeepers(t) // Create liquidity tiers and perpetuals, @@ -1038,10 +730,9 @@ func TestGetMarginRequirements_LiquidityTierNotFound(t *testing.T) { perpetualStore.Set(lib.Uint32ToKey(perpetual.Params.Id), b) // Getting margin requirements for perpetual with bad LiquidityTier should return an error. - _, _, err := pc.PerpetualsKeeper.GetMarginRequirements( + _, _, _, err := pc.PerpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier( pc.Ctx, perpetual.Params.Id, - big.NewInt(-1), ) require.EqualError( diff --git a/protocol/x/perpetuals/lib/lib.go b/protocol/x/perpetuals/lib/lib.go index 87cb0aab33..261ea1469e 100644 --- a/protocol/x/perpetuals/lib/lib.go +++ b/protocol/x/perpetuals/lib/lib.go @@ -35,6 +35,32 @@ func GetSettlementPpmWithPerpetual( return bigNetSettlementPpm, perpetual.FundingIndex.BigInt() } +// GetNetCollateralAndMarginRequirements returns the net collateral, initial margin requirement, +// and maintenance margin requirement in quote quantums, given the position size in base quantums. +func GetNetCollateralAndMarginRequirements( + perpetual types.Perpetual, + marketPrice pricestypes.MarketPrice, + liquidityTier types.LiquidityTier, + quantums *big.Int, +) ( + nc *big.Int, + imr *big.Int, + mmr *big.Int, +) { + nc = GetNetNotionalInQuoteQuantums( + perpetual, + marketPrice, + quantums, + ) + imr, mmr = GetMarginRequirementsInQuoteQuantums( + perpetual, + marketPrice, + liquidityTier, + quantums, + ) + return nc, imr, mmr +} + // GetNetNotionalInQuoteQuantums returns the net notional in quote quantums, which can be // represented by the following equation: // diff --git a/protocol/x/perpetuals/lib/lib_test.go b/protocol/x/perpetuals/lib/lib_test.go index 3723a86e91..52721a4a8c 100644 --- a/protocol/x/perpetuals/lib/lib_test.go +++ b/protocol/x/perpetuals/lib/lib_test.go @@ -1,6 +1,7 @@ package lib_test import ( + "math" "math/big" "testing" @@ -8,6 +9,7 @@ import ( "github.com/dydxprotocol/v4-chain/protocol/dtypes" big_testutil "github.com/dydxprotocol/v4-chain/protocol/testutil/big" + keepertest "github.com/dydxprotocol/v4-chain/protocol/testutil/keeper" "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/lib" "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" @@ -95,6 +97,72 @@ func TestGetSettlementPpmWithPerpetual(t *testing.T) { } } +func TestGetNetCollateralAndMarginRequirements(t *testing.T) { + testPerpetual := types.Perpetual{ + Params: types.PerpetualParams{ + AtomicResolution: -16, + }, + OpenInterest: dtypes.NewInt(1_000_000_000_000), + } + testMarketPrice := pricestypes.MarketPrice{ + Price: 123_456_789_123, + Exponent: -5, + } + testLiquidityTier := types.LiquidityTier{ + InitialMarginPpm: 200_000, + MaintenanceFractionPpm: 500_000, + } + tests := map[string]struct { + perpetual types.Perpetual + marketPrice pricestypes.MarketPrice + liquidityTier types.LiquidityTier + quantums *big.Int + }{ + "zero quantums": { + perpetual: testPerpetual, + marketPrice: testMarketPrice, + liquidityTier: testLiquidityTier, + quantums: big.NewInt(0), + }, + "positive quantums": { + perpetual: testPerpetual, + marketPrice: testMarketPrice, + liquidityTier: testLiquidityTier, + quantums: big.NewInt(1_000_000_000_000), + }, + "negative quantums": { + perpetual: testPerpetual, + marketPrice: testMarketPrice, + liquidityTier: testLiquidityTier, + quantums: big.NewInt(-1_000_000_000_000), + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + enc := lib.GetNetNotionalInQuoteQuantums( + test.perpetual, + test.marketPrice, + test.quantums, + ) + eimr, emmr := lib.GetMarginRequirementsInQuoteQuantums( + test.perpetual, + test.marketPrice, + test.liquidityTier, + test.quantums, + ) + nc, imr, mmr := lib.GetNetCollateralAndMarginRequirements( + test.perpetual, + test.marketPrice, + test.liquidityTier, + test.quantums, + ) + require.Equal(t, enc, nc) + require.Equal(t, eimr, imr) + require.Equal(t, emmr, mmr) + }) + } +} + func BenchmarkGetNetNotionalInQuoteQuantums(b *testing.B) { perpetual := types.Perpetual{ Params: types.PerpetualParams{ @@ -276,3 +344,313 @@ func TestGetMarginRequirementsInQuoteQuantums(t *testing.T) { }) } } + +func TestGetMarginRequirementsInQuoteQuantums_2(t *testing.T) { + oneBip := math.Pow10(2) + tests := map[string]struct { + price uint64 + exponent int32 + baseCurrencyAtomicResolution int32 + bigBaseQuantums *big.Int + initialMarginPpm uint32 + maintenanceFractionPpm uint32 + openInterest *big.Int + openInterestLowerCap uint64 + openInterestUpperCap uint64 + bigExpectedInitialMargin *big.Int + bigExpectedMaintenanceMargin *big.Int + }{ + "InitialMargin 2 BIPs, MaintenanceMargin 1 BIP, positive exponent, atomic resolution 8": { + price: 5_555, + exponent: 2, + baseCurrencyAtomicResolution: -8, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 2), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + bigExpectedInitialMargin: big.NewInt(7_777), + bigExpectedMaintenanceMargin: big.NewInt(3_889), + }, + "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, atomic resolution 4": { + price: 5_555, + exponent: 0, + baseCurrencyAtomicResolution: -4, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 100), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + bigExpectedInitialMargin: big.NewInt(38_885_000), + bigExpectedMaintenanceMargin: big.NewInt(19_442_500), + }, + "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, positive exponent, atomic resolution 0": { + price: 42, + exponent: 5, + baseCurrencyAtomicResolution: -0, + bigBaseQuantums: big.NewInt(88), + initialMarginPpm: uint32(oneBip * 100), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + bigExpectedInitialMargin: big.NewInt(3_696_000_000_000), + bigExpectedMaintenanceMargin: big.NewInt(1_848_000_000_000), + }, + "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, negative exponent, atomic resolution 6": { + price: 42_000_000, + exponent: -2, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(-5_000), + initialMarginPpm: uint32(oneBip * 100), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + bigExpectedInitialMargin: big.NewInt(21_000_000), + bigExpectedMaintenanceMargin: big.NewInt(10_500_000), + }, + "InitialMargin 10_000 BIPs (max), MaintenanceMargin 10_000 BIPs (max), atomic resolution 6": { + price: 5_555, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 10_000), + maintenanceFractionPpm: uint32(1_000_000), // 100% of IM + bigExpectedInitialMargin: big.NewInt(38_885_000), + bigExpectedMaintenanceMargin: big.NewInt(38_885_000), + }, + "InitialMargin 100 BIPs, MaintenanceMargin 100 BIPs, atomic resolution 6": { + price: 5_555, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 100), + maintenanceFractionPpm: uint32(1_000_000), // 100% of IM + bigExpectedInitialMargin: big.NewInt(388_850), + bigExpectedMaintenanceMargin: big.NewInt(388_850), + }, + "InitialMargin 0.02 BIPs, MaintenanceMargin 0.01 BIPs, positive exponent, atomic resolution 6": { + price: 5_555, + exponent: 3, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(-7_000), + initialMarginPpm: uint32(oneBip * 0.02), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + bigExpectedInitialMargin: big.NewInt(77_770), + bigExpectedMaintenanceMargin: big.NewInt(38_885), + }, + "InitialMargin 0 BIPs (min), MaintenanceMargin 0 BIPs (min), atomic resolution 6": { + price: 5_555, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 0), + maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, + bigExpectedInitialMargin: big.NewInt(0), + bigExpectedMaintenanceMargin: big.NewInt(0), + }, + "Price is zero, atomic resolution 6": { + price: 0, + exponent: 1, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(-7_000), + initialMarginPpm: uint32(oneBip * 1), + maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, + bigExpectedInitialMargin: big.NewInt(0), + bigExpectedMaintenanceMargin: big.NewInt(0), + }, + "Price and quantums are max uints": { + price: math.MaxUint64, + exponent: 1, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: new(big.Int).SetUint64(math.MaxUint64), + initialMarginPpm: uint32(oneBip * 1), + maintenanceFractionPpm: uint32(1_000_000), // 100% of IM, + bigExpectedInitialMargin: big_testutil.MustFirst( + new(big.Int).SetString("340282366920938463426481119284349109", 10), + ), + bigExpectedMaintenanceMargin: big_testutil.MustFirst( + new(big.Int).SetString("340282366920938463426481119284349109", 10), + ), + }, + "InitialMargin 100 BIPs, MaintenanceMargin 50 BIPs, atomic resolution 6": { + price: 5_555, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(7_000), + initialMarginPpm: uint32(oneBip * 100), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 + // = 10_000 * 38_885_000 / 1_000_000 ~= 388_850. + bigExpectedInitialMargin: big.NewInt(388_850), + bigExpectedMaintenanceMargin: big.NewInt(388_850 / 2), + }, + "InitialMargin 20%, MaintenanceMargin 10%, atomic resolution 6": { + price: 36_750, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(12_000), + initialMarginPpm: uint32(200_000), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + // quoteQuantums = 36_750 * 12_000 = 441_000_000 + // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 + // = 200_000 * 441_000_000 / 1_000_000 ~= 88_200_000 + bigExpectedInitialMargin: big.NewInt(88_200_000), + bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), + }, + "InitialMargin 5%, MaintenanceMargin 3%, atomic resolution 6": { + price: 123_456, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(74_523), + initialMarginPpm: uint32(50_000), + maintenanceFractionPpm: uint32(600_000), // 60% of IM + // quoteQuantums = 123_456 * 74_523 = 9_200_311_488 + // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 + // = 50_000 * 9_200_311_488 / 1_000_000 ~= 460_015_575 + bigExpectedInitialMargin: big.NewInt(460_015_575), + bigExpectedMaintenanceMargin: big.NewInt(276_009_345), + }, + "InitialMargin 25%, MaintenanceMargin 15%, atomic resolution 6": { + price: 123_456, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(74_523), + initialMarginPpm: uint32(250_000), + maintenanceFractionPpm: uint32(600_000), // 60% of IM + // quoteQuantums = 123_456 * 74_523 = 9_200_311_488 + bigExpectedInitialMargin: big.NewInt(2_300_077_872), + bigExpectedMaintenanceMargin: big.NewInt(1_380_046_724), // Rounded up + }, + "OIMF: IM 20%, scaled to 60%, MaintenanceMargin 10%, atomic resolution 6": { + price: 36_750, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(12_000), + initialMarginPpm: uint32(200_000), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + openInterest: big.NewInt(408_163_265), // 408.163265 + openInterestLowerCap: 10_000_000_000_000, + openInterestUpperCap: 20_000_000_000_000, + // openInterestNotional = 408_163_265 * 36_750 = 14_999_999_988_750 + // percentageOfCap = (openInterestNotional - lowerCap) / (upperCap - lowerCap) = 0.499999998875 + // adjustedIMF = (0.499999998875) * 0.8 + 0.2 = 0.5999999991 (rounded is 599_999 ppm) + // bigExpectedInitialMargin = bigBaseQuantums * price * adjustedIMF = 264_599_559 + bigExpectedInitialMargin: big.NewInt(264_599_559), + bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), + }, + "OIMF: IM 20%, scaled to 100%, MaintenanceMargin 10%, atomic resolution 6": { + price: 36_750, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(12_000), + initialMarginPpm: uint32(200_000), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + openInterest: big.NewInt(1_000_000_000), // 1000 or ~$36mm notional + openInterestLowerCap: 10_000_000_000_000, + openInterestUpperCap: 20_000_000_000_000, + // quoteQuantums = 36_750 * 12_000 = 441_000_000 + // initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000 + // = 200_000 * 441_000_000 / 1_000_000 ~= 88_200_000 + bigExpectedInitialMargin: big.NewInt(441_000_000), + bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), + }, + "OIMF: IM 20%, lower_cap < realistic open interest < upper_cap, MaintenanceMargin 10%, atomic resolution 6": { + price: 36_750, + exponent: 0, + baseCurrencyAtomicResolution: -6, + bigBaseQuantums: big.NewInt(12_000), + initialMarginPpm: uint32(200_000), + maintenanceFractionPpm: uint32(500_000), // 50% of IM + openInterest: big.NewInt(1_123_456_789), // 1123.456 or ~$41mm notional + openInterestLowerCap: 25_000_000_000_000, + openInterestUpperCap: 50_000_000_000_000, + // openInterestNotional = 1_123_456_789 * 36_750 = 41_287_036_995_750 + // percentageOfCap = (openInterestNotional - lowerCap) / (upperCap - lowerCap) = 0.65148147983 + // adjustedIMF = (0.65148147983) * 0.8 + 0.2 = 0.721185183864 (rounded is 721_185 ppm) + // bigExpectedInitialMargin = bigBaseQuantums * price * adjustedIMF = 318_042_585 + bigExpectedInitialMargin: big.NewInt(318_042_585), + bigExpectedMaintenanceMargin: big.NewInt(88_200_000 / 2), + }, + } + + // Run tests. + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Individual test setup. + pc := keepertest.PerpetualsKeepers(t) + // Create a new market param and price. + marketId := keepertest.GetNumMarkets(t, pc.Ctx, pc.PricesKeeper) + _, err := pc.PricesKeeper.CreateMarket( + pc.Ctx, + pricestypes.MarketParam{ + Id: marketId, + Pair: "marketName", + Exponent: tc.exponent, + MinExchanges: uint32(1), + MinPriceChangePpm: uint32(50), + ExchangeConfigJson: "{}", + }, + pricestypes.MarketPrice{ + Id: marketId, + Exponent: tc.exponent, + Price: 1_000, // leave this as a placeholder b/c we cannot set the price to 0 + }, + ) + require.NoError(t, err) + + // Update `Market.price`. By updating prices this way, we can simulate conditions where the oracle + // price may become 0. + err = pc.PricesKeeper.UpdateMarketPrices( + pc.Ctx, + []*pricestypes.MsgUpdateMarketPrices_MarketPrice{pricestypes.NewMarketPriceUpdate( + marketId, + tc.price, + )}, + ) + require.NoError(t, err) + + // Create `LiquidityTier` struct. + _, err = pc.PerpetualsKeeper.SetLiquidityTier( + pc.Ctx, + 0, + "name", + tc.initialMarginPpm, + tc.maintenanceFractionPpm, + 1, // dummy impact notional value + tc.openInterestLowerCap, + tc.openInterestUpperCap, + ) + require.NoError(t, err) + + // Create `Perpetual` struct with baseAssetAtomicResolution and marketId. + perpetual, err := pc.PerpetualsKeeper.CreatePerpetual( + pc.Ctx, + 0, // PerpetualId + "getMarginRequirementsTicker", // Ticker + marketId, // MarketId + tc.baseCurrencyAtomicResolution, // AtomicResolution + int32(0), // DefaultFundingPpm + 0, // LiquidityTier + types.PerpetualMarketType_PERPETUAL_MARKET_TYPE_CROSS, // MarketType + ) + require.NoError(t, err) + + // If test case contains non-nil open interest, set it up. + if tc.openInterest != nil { + require.NoError(t, pc.PerpetualsKeeper.ModifyOpenInterest( + pc.Ctx, + perpetual.Params.Id, + tc.openInterest, // initialized as zero, so passing `openInterest` as delta amount. + )) + } + + // Verify initial and maintenance margin requirements are calculated correctly. + perpetual, marketPrice, liquidityTier, err := pc.PerpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier( + pc.Ctx, + perpetual.Params.Id, + ) + require.NoError(t, err) + imr, mmr := lib.GetMarginRequirementsInQuoteQuantums( + perpetual, + marketPrice, + liquidityTier, + tc.bigBaseQuantums, + ) + + require.Equal(t, tc.bigExpectedInitialMargin, imr, "Initial margin mismatch") + require.Equal(t, tc.bigExpectedMaintenanceMargin, mmr, "Maintenance margin mismatch") + }) + } +} diff --git a/protocol/x/perpetuals/types/types.go b/protocol/x/perpetuals/types/types.go index 5dd354107b..0da2437d97 100644 --- a/protocol/x/perpetuals/types/types.go +++ b/protocol/x/perpetuals/types/types.go @@ -47,15 +47,6 @@ type PerpetualsKeeper interface { bigNetCollateralQuoteQuantums *big.Int, err error, ) - GetMarginRequirements( - ctx sdk.Context, - id uint32, - bigQuantums *big.Int, - ) ( - bigInitialMarginQuoteQuantums *big.Int, - bigMaintenanceMarginQuoteQuantums *big.Int, - err error, - ) GetAddPremiumVotes( ctx sdk.Context, ) ( diff --git a/protocol/x/subaccounts/keeper/subaccount.go b/protocol/x/subaccounts/keeper/subaccount.go index a937757fe4..69b623efc5 100644 --- a/protocol/x/subaccounts/keeper/subaccount.go +++ b/protocol/x/subaccounts/keeper/subaccount.go @@ -931,51 +931,47 @@ func (k Keeper) internalGetNetCollateralAndMarginRequirements( return big.NewInt(0), big.NewInt(0), big.NewInt(0), err } - // The calculate function increments `netCollateral`, `initialMargin`, and `maintenanceMargin` - // given a `ProductKeeper` and a `PositionSize`. - calculate := func(pk types.ProductKeeper, size types.PositionSize) error { + // Iterate over all assets and updates and calculate change to net collateral and margin requirements. + for _, size := range assetSizes { id := size.GetId() bigQuantums := size.GetBigQuantums() - bigNetCollateralQuoteQuantums, err := pk.GetNetCollateral(ctx, id, bigQuantums) + nc, err := k.assetsKeeper.GetNetCollateral(ctx, id, bigQuantums) if err != nil { - return err + return big.NewInt(0), big.NewInt(0), big.NewInt(0), err } - bigNetCollateral.Add(bigNetCollateral, bigNetCollateralQuoteQuantums) - - bigInitialMarginRequirements, - bigMaintenanceMarginRequirements, - err := pk.GetMarginRequirements( + imr, mmr, err := k.assetsKeeper.GetMarginRequirements( ctx, id, bigQuantums, ) - if err != nil { - return err - } - - bigInitialMargin.Add(bigInitialMargin, bigInitialMarginRequirements) - bigMaintenanceMargin.Add(bigMaintenanceMargin, bigMaintenanceMarginRequirements) - - return nil - } - - // Iterate over all assets and updates and calculate change to net collateral and margin requirements. - for _, size := range assetSizes { - err := calculate(k.assetsKeeper, size) if err != nil { return big.NewInt(0), big.NewInt(0), big.NewInt(0), err } + bigNetCollateral.Add(bigNetCollateral, nc) + bigInitialMargin.Add(bigInitialMargin, imr) + bigMaintenanceMargin.Add(bigMaintenanceMargin, mmr) } // Iterate over all perpetuals and updates and calculate change to net collateral and margin requirements. - // TODO(DEC-110): `perp.GetSettlement()`, factor in unsettled funding. for _, size := range perpetualSizes { - err := calculate(k.perpetualsKeeper, size) + perpetual, + marketPrice, + liquidityTier, + err := k.perpetualsKeeper.GetPerpetualAndMarketPriceAndLiquidityTier(ctx, size.GetId()) if err != nil { return big.NewInt(0), big.NewInt(0), big.NewInt(0), err } + nc, imr, mmr := perplib.GetNetCollateralAndMarginRequirements( + perpetual, + marketPrice, + liquidityTier, + size.GetBigQuantums(), + ) + bigNetCollateral.Add(bigNetCollateral, nc) + bigInitialMargin.Add(bigInitialMargin, imr) + bigMaintenanceMargin.Add(bigMaintenanceMargin, mmr) } return bigNetCollateral, bigInitialMargin, bigMaintenanceMargin, nil diff --git a/protocol/x/subaccounts/types/expected_keepers.go b/protocol/x/subaccounts/types/expected_keepers.go index 201f8352fc..2e3372c973 100644 --- a/protocol/x/subaccounts/types/expected_keepers.go +++ b/protocol/x/subaccounts/types/expected_keepers.go @@ -8,11 +8,23 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" blocktimetypes "github.com/dydxprotocol/v4-chain/protocol/x/blocktime/types" perptypes "github.com/dydxprotocol/v4-chain/protocol/x/perpetuals/types" + pricestypes "github.com/dydxprotocol/v4-chain/protocol/x/prices/types" ) // ProductKeeper represents a generic interface for a keeper // of a product. type ProductKeeper interface { + IsPositionUpdatable( + ctx sdk.Context, + id uint32, + ) ( + updatable bool, + err error, + ) +} + +type AssetsKeeper interface { + ProductKeeper GetNetCollateral( ctx sdk.Context, id uint32, @@ -30,17 +42,6 @@ type ProductKeeper interface { bigMaintenanceMarginQuoteQuantums *big.Int, err error, ) - IsPositionUpdatable( - ctx sdk.Context, - id uint32, - ) ( - updatable bool, - err error, - ) -} - -type AssetsKeeper interface { - ProductKeeper ConvertAssetToCoin( ctx sdk.Context, assetId uint32, @@ -71,6 +72,15 @@ type PerpetualsKeeper interface { perpetual perptypes.Perpetual, err error, ) + GetPerpetualAndMarketPriceAndLiquidityTier( + ctx sdk.Context, + perpetualId uint32, + ) ( + perptypes.Perpetual, + pricestypes.MarketPrice, + perptypes.LiquidityTier, + error, + ) GetAllPerpetuals(ctx sdk.Context) []perptypes.Perpetual GetInsuranceFundName(ctx sdk.Context, perpetualId uint32) (string, error) GetInsuranceFundModuleAddress(ctx sdk.Context, perpetualId uint32) (sdk.AccAddress, error)