Skip to content

Commit

Permalink
[OTE-214] Calculate current OI and pass to GetMarginRequirements ca…
Browse files Browse the repository at this point in the history
…lculation (#1161)

* pass in oi

* add test case for `ModifyOpenInterest` failure
  • Loading branch information
teddyding authored Mar 13, 2024
1 parent 3a2ede6 commit 0d41406
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 6 deletions.
54 changes: 50 additions & 4 deletions protocol/x/perpetuals/keeper/perpetual.go
Original file line number Diff line number Diff line change
Expand Up @@ -1008,27 +1008,39 @@ func GetMarginRequirementsInQuoteQuantums(
// Always consider the magnitude of the position regardless of whether it is long/short.
bigAbsQuantums := new(big.Int).Set(bigQuantums).Abs(bigQuantums)

// Calculate the notional value of the position in quote quantums.
bigQuoteQuantums := lib.BaseToQuoteQuantums(
bigAbsQuantums,
perpetual.Params.AtomicResolution,
marketPrice.Price,
marketPrice.Exponent,
)
// Calculate the perpetual's open interest in quote quantums.
openInterestQuoteQuantums := lib.BaseToQuoteQuantums(
perpetual.OpenInterest.BigInt(), // OpenInterest is represented as base quantums.
perpetual.Params.AtomicResolution,
marketPrice.Price,
marketPrice.Exponent,
)

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

// Maintenance margin requirement quote quantums = IM in quote quantums * maintenance fraction PPM.
bigMaintenanceMarginQuoteQuantums = lib.BigRatRound(
lib.BigRatMulPpm(
new(big.Rat).SetInt(bigInitialMarginQuoteQuantums),
new(big.Rat).SetInt(bigBaseInitialMarginQuoteQuantums),
liquidityTier.MaintenanceFractionPpm,
),
true,
)

bigInitialMarginQuoteQuantums = liquidityTier.GetInitialMarginQuoteQuantums(
bigQuoteQuantums,
openInterestQuoteQuantums, // pass in current OI to get scaled IMR.
)
return bigInitialMarginQuoteQuantums, bigMaintenanceMarginQuoteQuantums
}

Expand Down Expand Up @@ -1222,6 +1234,40 @@ func (k Keeper) ModifyFundingIndex(
return nil
}

// Modify the open interest of a perpetual in state.
func (k Keeper) ModifyOpenInterest(
ctx sdk.Context,
perpetualId uint32,
openInterestDeltaBaseQuantums *big.Int,
) (
err error,
) {
// Get perpetual.
perpetual, err := k.GetPerpetual(ctx, perpetualId)
if err != nil {
return err
}

bigOpenInterest := perpetual.OpenInterest.BigInt()
bigOpenInterest.Add(
bigOpenInterest, // reuse pointer for efficiency
openInterestDeltaBaseQuantums,
)

if bigOpenInterest.Sign() < 0 {
return errorsmod.Wrapf(
types.ErrOpenInterestWouldBecomeNegative,
"perpetualId = %d, openInterest = %s",
perpetualId,
bigOpenInterest.String(),
)
}

perpetual.OpenInterest = dtypes.NewIntFromBigInt(bigOpenInterest)
k.SetPerpetual(ctx, perpetual)
return nil
}

// SetEmptyPremiumSamples initializes empty premium samples for all perpetuals
func (k Keeper) SetEmptyPremiumSamples(
ctx sdk.Context,
Expand Down
191 changes: 189 additions & 2 deletions protocol/x/perpetuals/keeper/perpetual_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,132 @@ func TestGetAllPerpetuals_Sorted(t *testing.T) {
)
}

func TestModifyOpenInterest_Failure(t *testing.T) {
testCases := map[string]struct {
id uint32
initOpenInterest *big.Int
openInterestDelta *big.Int
err error
}{
"Would become negative": {
id: 0,
initOpenInterest: big.NewInt(1_000),
openInterestDelta: big.NewInt(-1_001),
err: types.ErrOpenInterestWouldBecomeNegative,
},
"Non-existent perp Id": {
id: 1111,
initOpenInterest: big.NewInt(1_000),
openInterestDelta: big.NewInt(0),
err: types.ErrPerpetualDoesNotExist,
},
}

for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
pc := keepertest.PerpetualsKeepers(t)
perps := keepertest.CreateLiquidityTiersAndNPerpetuals(t, pc.Ctx, pc.PerpetualsKeeper, pc.PricesKeeper, 1)

// Set up initial open interest
require.NoError(t, pc.PerpetualsKeeper.ModifyOpenInterest(
pc.Ctx,
perps[0].Params.Id,
tc.initOpenInterest,
))

err := pc.PerpetualsKeeper.ModifyOpenInterest(
pc.Ctx,
tc.id,
tc.openInterestDelta,
)
require.ErrorContains(t, err, tc.err.Error())
})
}
}

func TestModifyOpenInterest_Mixed(t *testing.T) {
pc := keepertest.PerpetualsKeepers(t)
// Create liquidity tiers and perpetuals,
perps := keepertest.CreateLiquidityTiersAndNPerpetuals(t, pc.Ctx, pc.PerpetualsKeeper, pc.PricesKeeper, 100)

for _, perp := range perps {
openInterestDeltaBaseQuantums := big.NewInt(2_000_000_000*(int64(perp.Params.Id)%2) - 1)

// Add `openInterestDeltaBaseQuantums` to open interest which is initially 0.
err := pc.PerpetualsKeeper.ModifyOpenInterest(
pc.Ctx,
perp.Params.Id,
openInterestDeltaBaseQuantums,
)

// If Id is even, the modification is negagive and should fail.
if perp.Params.Id%2 == 0 {
require.ErrorContains(t,
err,
types.ErrOpenInterestWouldBecomeNegative.Error(),
)

newPerp, err := pc.PerpetualsKeeper.GetPerpetual(pc.Ctx, perp.Params.Id)
require.NoError(t, err)

require.Equal(
t,
big.NewInt(0), // open interest should remain 0
newPerp.OpenInterest.BigInt(),
)
} else {
require.NoError(t, err)

newPerp, err := pc.PerpetualsKeeper.GetPerpetual(pc.Ctx, perp.Params.Id)
require.NoError(t, err)

require.Equal(
t,
openInterestDeltaBaseQuantums,
newPerp.OpenInterest.BigInt(),
)
}

// Add `openInterestDeltaBaseQuantums` again
err = pc.PerpetualsKeeper.ModifyOpenInterest(
pc.Ctx,
perp.Params.Id,
openInterestDeltaBaseQuantums,
)
// If Id is even, the modification is negagive and should fail.
if perp.Params.Id%2 == 0 {
require.ErrorContains(t,
err,
types.ErrOpenInterestWouldBecomeNegative.Error(),
)

newPerp, err := pc.PerpetualsKeeper.GetPerpetual(pc.Ctx, perp.Params.Id)
require.NoError(t, err)

require.Equal(
t,
big.NewInt(0), // open interest should remain 0
newPerp.OpenInterest.BigInt(),
)
} else {
require.NoError(t, err)

newPerp, err := pc.PerpetualsKeeper.GetPerpetual(pc.Ctx, perp.Params.Id)
require.NoError(t, err)

require.Equal(
t,
// open interest should be 2 * delta now
openInterestDeltaBaseQuantums.Mul(
openInterestDeltaBaseQuantums,
big.NewInt(2),
),
newPerp.OpenInterest.BigInt(),
)
}
}
}

func TestGetMarginRequirements_Success(t *testing.T) {
oneBip := math.Pow10(2)
tests := map[string]struct {
Expand All @@ -561,6 +687,9 @@ func TestGetMarginRequirements_Success(t *testing.T) {
bigBaseQuantums *big.Int
initialMarginPpm uint32
maintenanceFractionPpm uint32
openInterest *big.Int
openInterestLowerCap uint64
openInterestUpperCap uint64
bigExpectedInitialMarginPpm *big.Int
bigExpectedMaintenanceMarginPpm *big.Int
}{
Expand Down Expand Up @@ -717,6 +846,55 @@ func TestGetMarginRequirements_Success(t *testing.T) {
bigExpectedInitialMarginPpm: big.NewInt(2_300_077_872),
bigExpectedMaintenanceMarginPpm: 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,
// 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
bigExpectedInitialMarginPpm: big.NewInt(88_200_000 * 3),
bigExpectedMaintenanceMarginPpm: 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
bigExpectedInitialMarginPpm: big.NewInt(441_000_000),
bigExpectedMaintenanceMarginPpm: 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,
// quoteQuantums = 36_750 * 12_000 = 441_000_000
// initialMarginPpmQuoteQuantums = initialMarginPpm * quoteQuantums / 1_000_000
// = ((1123.456789 * 36750 - 25000000) / 25000000 * 0.8 + 0.2) * 441_000_000
// ~= 318042667
bigExpectedInitialMarginPpm: big.NewInt(318_042_667),
bigExpectedMaintenanceMarginPpm: big.NewInt(88_200_000 / 2),
},
}

// Run tests.
Expand Down Expand Up @@ -763,8 +941,8 @@ func TestGetMarginRequirements_Success(t *testing.T) {
tc.initialMarginPpm,
tc.maintenanceFractionPpm,
1, // dummy impact notional value
0, // dummy open interest lower cap
0, // dummy open interest upper cap
tc.openInterestLowerCap,
tc.openInterestUpperCap,
)
require.NoError(t, err)

Expand All @@ -781,6 +959,15 @@ func TestGetMarginRequirements_Success(t *testing.T) {
)
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,
Expand Down
5 changes: 5 additions & 0 deletions protocol/x/perpetuals/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ var (
24,
"open interest lower cap is larger than upper cap",
)
ErrOpenInterestWouldBecomeNegative = errorsmod.Register(
ModuleName,
25,
"open interest would become negative after update",
)

// Errors for Not Implemented
ErrNotImplementedFunding = errorsmod.Register(ModuleName, 1001, "Not Implemented: Perpetuals Funding")
Expand Down

0 comments on commit 0d41406

Please sign in to comment.