Skip to content

Commit

Permalink
[OTE-162] Modify GetInitialMarginQuoteQuantums to reflect OIMF (#1159)
Browse files Browse the repository at this point in the history
* Modify `GetInitialMarginQuoteQuantums` to reflect OIMF

* address comments

* nit
  • Loading branch information
teddyding authored Mar 12, 2024
1 parent 525bb6f commit 3a2ede6
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 11 deletions.
5 changes: 4 additions & 1 deletion protocol/x/perpetuals/keeper/perpetual.go
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,10 @@ func GetMarginRequirementsInQuoteQuantums(
)

// Initial margin requirement quote quantums = size in quote quantums * initial margin PPM.
bigInitialMarginQuoteQuantums = liquidityTier.GetInitialMarginQuoteQuantums(bigQuoteQuantums)
bigInitialMarginQuoteQuantums = liquidityTier.GetInitialMarginQuoteQuantums(
bigQuoteQuantums,
big.NewInt(0), // Temporary for open interest notional. TODO(OTE-214): replace with actual value.
)

// Maintenance margin requirement quote quantums = IM in quote quantums * maintenance fraction PPM.
bigMaintenanceMarginQuoteQuantums = lib.BigRatRound(
Expand Down
111 changes: 103 additions & 8 deletions protocol/x/perpetuals/types/liquidity_tier.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package types

import (
"fmt"
"math/big"

errorsmod "cosmossdk.io/errors"
Expand Down Expand Up @@ -72,14 +73,108 @@ func (liquidityTier LiquidityTier) GetMaxAbsFundingClampPpm(clampFactorPpm uint3
)
}

// GetInitialMarginQuoteQuantums returns initial margin in quote quantums.
// GetInitialMarginQuoteQuantums returns initial margin requirement (IMR) in quote quantums.
//
// marginQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000
// Now that OIMF is introduced, the calculation of IMR is as follows:
//
// note: divisions are delayed for precision purposes.
func (liquidityTier LiquidityTier) GetInitialMarginQuoteQuantums(bigQuoteQuantums *big.Int) *big.Int {
result := new(big.Rat).SetInt(bigQuoteQuantums)
// Multiply above result with `initialMarginPpm` and divide by 1 million.
result = lib.BigRatMulPpm(result, liquidityTier.InitialMarginPpm)
return lib.BigRatRound(result, true) // Round up initial margin.
// - Each market has a `Lower Cap` and `Upper Cap` denominated in USDC.
// - Each market already has a `Base IMF`.
// - At any point in time, for each market:
// - Define
// - `Open Notional = Open Interest * Oracle Price`
// - `Scaling Factor = (Open Notional - Lower Cap) / (Upper Cap - Lower Cap)`
// - `IMF Increase = Scaling Factor * (1 - Base IMF)`
// - Then a market’s `Effective IMF = Min(Base IMF + Max(IMF Increase, 0), 1.0)`
//
// - I.e. the effective IMF is the base IMF while the OI < lower cap, and increases linearly until OI = Upper Cap,
// at which point the IMF stays at 1.0 (requiring 1:1 collateral for trading).
// - initialMarginQuoteQuantums = scaledInitialMarginPpm * quoteQuantums / 1_000_000
//
// note:
// - divisions are delayed for precision purposes.
func (liquidityTier LiquidityTier) GetInitialMarginQuoteQuantums(
bigQuoteQuantums *big.Int,
openInterestQuoteQuantums *big.Int,
) *big.Int {
bigOpenInterestUpperCap := new(big.Int).SetUint64(liquidityTier.OpenInterestUpperCap)

// If `open_interest` >= `open_interest_upper_cap` where `upper_cap` is non-zero,
// OIMF = 1.0 so return input quote quantums as the IMR.
if openInterestQuoteQuantums.Cmp(
bigOpenInterestUpperCap,
) >= 0 && liquidityTier.OpenInterestUpperCap != 0 {
return bigQuoteQuantums
}

ratQuoteQuantums := new(big.Rat).SetInt(bigQuoteQuantums)

// If `open_interest_upper_cap` is 0, OIMF is disabled。
// Or if `current_interest` <= `open_interest_lower_cap`, IMF is not scaled.
// In both cases, use base IMF as OIMF.
bigOpenInterestLowerCap := new(big.Int).SetUint64(liquidityTier.OpenInterestLowerCap)
if liquidityTier.OpenInterestUpperCap == 0 || openInterestQuoteQuantums.Cmp(
bigOpenInterestLowerCap,
) <= 0 {
// Calculate base IMR: multiply `bigQuoteQuantums` with `initialMarginPpm` and divide by 1 million.
ratBaseIMR := lib.BigRatMulPpm(ratQuoteQuantums, liquidityTier.InitialMarginPpm)
return lib.BigRatRound(ratBaseIMR, true) // Round up initial margin.
}

// If `open_interest_lower_cap` < `open_interest` <= `open_interest_upper_cap`, calculate the scaled OIMF.
// `Scaling Factor = (Open Notional - Lower Cap) / (Upper Cap - Lower Cap)`
ratScalingFactor := new(big.Rat).SetFrac(
new(big.Int).Sub(
openInterestQuoteQuantums, // reuse pointer for memory efficiency
bigOpenInterestLowerCap,
),
bigOpenInterestUpperCap.Sub(
bigOpenInterestUpperCap, // reuse pointer for memory efficiency
bigOpenInterestLowerCap,
),
)

// `IMF Increase = Scaling Factor * (1 - Base IMF)`
ratIMFIncrease := lib.BigRatMulPpm(
ratScalingFactor,
lib.OneMillion-liquidityTier.InitialMarginPpm, // >= 0, since we check in `liquidityTier.Validate()`
)

// Calculate `Max(IMF Increase, 0)`.
if ratIMFIncrease.Sign() < 0 {
panic(
fmt.Sprintf(
"GetInitialMarginQuoteQuantums: IMF Increase is negative (%s), liquidityTier: %+v, openInterestQuoteQuantums: %s",
ratIMFIncrease.String(),
liquidityTier,
openInterestQuoteQuantums.String(),
),
)
}

// First, calculate base IMF in big.Rat
ratBaseIMF := new(big.Rat).SetFrac64(
int64(liquidityTier.InitialMarginPpm), // safe, since `InitialMargin` is uint32
int64(lib.OneMillion),
)

// `Effective IMF = Min(Base IMF + Max(IMF Increase, 0), 1.0)`
ratEffectiveIMF := ratBaseIMF.Add(
ratBaseIMF, // reuse pointer for memory efficiency
ratIMFIncrease,
)

// `Effective IMR = Effective IMF * Quote Quantums`
bigIMREffective := lib.BigRatRound(
ratEffectiveIMF.Mul(
ratEffectiveIMF, // reuse pointer for memory efficiency
ratQuoteQuantums,
),
true, // Round up initial margin.
)

// Return min(Effective IMR, Quote Quantums)
if bigIMREffective.Cmp(bigQuoteQuantums) >= 0 {
return bigQuoteQuantums
}
return bigIMREffective
}
96 changes: 94 additions & 2 deletions protocol/x/perpetuals/types/liquidity_tier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ func TestLiquidityTierValidate(t *testing.T) {
initialMarginPpm uint32
maintenanceFractionPpm uint32
ImpactNotional uint64
openInterestLowerCap uint64
openInterestUpperCap uint64
expectedError error
}{
"Validates successfully": {
Expand All @@ -40,6 +42,22 @@ func TestLiquidityTierValidate(t *testing.T) {
ImpactNotional: 0, // 0
expectedError: types.ErrImpactNotionalIsZero,
},
"Failure: lower cap is larger than upper cap": {
initialMarginPpm: 150_000, // 15%
maintenanceFractionPpm: 800_000, // 80% of IM
ImpactNotional: 3_333_000_000, // 3_333 USDC
openInterestLowerCap: 1_000_000,
openInterestUpperCap: 500_000,
expectedError: types.ErrOpenInterestLowerCapLargerThanUpperCap,
},
"Failure: lower cap is larger than upper cap (upper cap is zero)": {
initialMarginPpm: 150_000, // 15%
maintenanceFractionPpm: 800_000, // 80% of IM
ImpactNotional: 3_333_000_000, // 3_333 USDC
openInterestLowerCap: 1_000_000,
openInterestUpperCap: 0,
expectedError: types.ErrOpenInterestLowerCapLargerThanUpperCap,
},
}

// Run tests.
Expand All @@ -49,6 +67,8 @@ func TestLiquidityTierValidate(t *testing.T) {
InitialMarginPpm: tc.initialMarginPpm,
MaintenanceFractionPpm: tc.maintenanceFractionPpm,
ImpactNotional: tc.ImpactNotional,
OpenInterestLowerCap: tc.openInterestLowerCap,
OpenInterestUpperCap: tc.openInterestUpperCap,
}

err := liquidityTier.Validate()
Expand Down Expand Up @@ -202,6 +222,9 @@ func TestLiquidityTierGetMaxAbsFundingClampPpm(t *testing.T) {
func TestGetInitialMarginQuoteQuantums(t *testing.T) {
tests := map[string]struct {
initialMarginPpm uint32
openInterestLowerCap uint64
openInterestUpperCap uint64
openInterestNotional *big.Int
bigQuoteQuantums *big.Int
expectedInitialMarginQuoteQuantums *big.Int
}{
Expand Down Expand Up @@ -237,13 +260,82 @@ func TestGetInitialMarginQuoteQuantums(t *testing.T) {
// ~= 70029.5518 -> round up to 70030
expectedInitialMarginQuoteQuantums: big.NewInt(70_030),
},
"base IMF = 20%, no OIMF since lower_cap = upper_cap = 0": {
initialMarginPpm: uint32(200_000), // 20%
bigQuoteQuantums: big.NewInt(500_000),
openInterestNotional: big.NewInt(1_500_000_000_000),
openInterestLowerCap: 0,
openInterestUpperCap: 0,
// initial margin * quote quantums
// = 20% * 500_000
// = 100_000
expectedInitialMarginQuoteQuantums: big.NewInt(100_000),
},
"base IMF = 20%, scaling_factor = 0.5": {
initialMarginPpm: uint32(200_000), // 20%
bigQuoteQuantums: big.NewInt(500_000),
openInterestNotional: big.NewInt(1_500_000_000_000),
openInterestLowerCap: 1_000_000_000_000,
openInterestUpperCap: 2_000_000_000_000,
// OIMF = 20% + 0.5 * (1 - 20%) = 20% + 0.5 * 80% = 60%
// initial margin * quote quantums
// = 60% * 100% * 500_000
// = 300_000
expectedInitialMarginQuoteQuantums: big.NewInt(300_000),
},
"base IMF = 10%, scaling_factor = 1 since open_interest >> upper_cap": {
initialMarginPpm: uint32(200_000), // 20%
bigQuoteQuantums: big.NewInt(500_000),
openInterestNotional: big.NewInt(80_000_000_000_000),
openInterestLowerCap: 25_000_000_000_000,
openInterestUpperCap: 50_000_000_000_000,
// OIMF = 100%
// initial margin * quote quantums
// = 100% * 100% * 500_000
// = 500_000
expectedInitialMarginQuoteQuantums: big.NewInt(500_000),
},
"base IMF = 10%, open_interest = lower_cap so scaling_factor = 0": {
initialMarginPpm: uint32(200_000), // 20%
bigQuoteQuantums: big.NewInt(500_000),
openInterestNotional: big.NewInt(25_000_000_000_000),
openInterestLowerCap: 25_000_000_000_000,
openInterestUpperCap: 50_000_000_000_000,
// OIMF = 20%
// initial margin * quote quantums
// = 20% * 100% * 500_000
// = 100_000
expectedInitialMarginQuoteQuantums: big.NewInt(100_000),
},
"base IMF = 10%, lower_cap < open_interest < upper_cap, realistic numbers": {
initialMarginPpm: uint32(200_000), // 20%
bigQuoteQuantums: big.NewInt(500_000),
openInterestNotional: big.NewInt(28_123_456_789_123),
openInterestLowerCap: 25_000_000_000_000,
openInterestUpperCap: 60_000_000_000_000,
// scaling_factor = (28.123 - 25) / (60 - 25) ~= 0.08924
// OIMF ~= 0.08924 * 80% + 20%
// ~= 71392% + 20%
// ~= 27.1392%
// initial margin * quote quantums
// = 27.1392% * 500_000
// = 135_697
expectedInitialMarginQuoteQuantums: big.NewInt(135_697),
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
liquidityTier := &types.LiquidityTier{
InitialMarginPpm: tc.initialMarginPpm,
InitialMarginPpm: tc.initialMarginPpm,
OpenInterestLowerCap: tc.openInterestLowerCap,
OpenInterestUpperCap: tc.openInterestUpperCap,
}

openInterestNotional := big.NewInt(0)
if tc.openInterestNotional != nil {
openInterestNotional.Set(tc.openInterestNotional)
}
adjustedIMQuoteQuantums := liquidityTier.GetInitialMarginQuoteQuantums(tc.bigQuoteQuantums)
adjustedIMQuoteQuantums := liquidityTier.GetInitialMarginQuoteQuantums(tc.bigQuoteQuantums, openInterestNotional)

require.Equal(t, tc.expectedInitialMarginQuoteQuantums, adjustedIMQuoteQuantums)
})
Expand Down

0 comments on commit 3a2ede6

Please sign in to comment.