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(perp): FluctuationLimit check on OpenPosition #767

Merged
merged 11 commits into from
Aug 2, 2022
Merged
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Improvements

* [#775](https://github.com/NibiruChain/nibiru/pull/775) - bump google.golang.org/protobuf from 1.28.0 to 1.28.1
* [#768](https://github.com/NibiruChain/nibiru/pull/768/) - add simulation tests to make file
* [#768](https://github.com/NibiruChain/nibiru/pull/768) - add simulation tests to make file
* [#767](https://github.com/NibiruChain/nibiru/pull/767) - add fluctuation limit checks on `OpenPosition`.

### Bug Fixes

Expand Down Expand Up @@ -125,4 +126,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* [#695](https://github.com/NibiruChain/nibiru/pull/695) Add `OpenPosition` integration tests.
* [#692](https://github.com/NibiruChain/nibiru/pull/692) Add test coverage for Perp MsgServer methods.
* [#768](https://github.com/NibiruChain/nibiru/pull/768) Update simulation tests to read flags over static values
47 changes: 27 additions & 20 deletions x/perp/keeper/clearing_house.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ func (k Keeper) OpenPosition(
/* quoteAssetAmount */ quoteAssetAmount.ToDec(),
/* leverage */ leverage,
/* baseAmtLimit */ baseAmtLimit,
/* skipFluctuationLimitCheck */ false,
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -243,7 +242,7 @@ func (k Keeper) increasePosition(
side,
increasedNotional,
baseAmtLimit,
false,
/* skipFluctuationLimitCheck */ false,
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -296,7 +295,6 @@ func (k Keeper) openReversePosition(
quoteAssetAmount sdk.Dec,
leverage sdk.Dec,
baseAmtLimit sdk.Dec,
skipFluctuationLimitCheck bool,
) (positionResp *types.PositionResp, err error) {
notionalToDecreaseBy := leverage.Mul(quoteAssetAmount)
currentPositionNotional, _, err := k.getPositionNotionalAndUnrealizedPnL(
Expand All @@ -315,7 +313,7 @@ func (k Keeper) openReversePosition(
currentPosition,
notionalToDecreaseBy,
baseAmtLimit,
skipFluctuationLimitCheck,
/* skipFluctuationLimitCheck */ false,
)
} else {
// close and reverse
Expand Down Expand Up @@ -350,16 +348,18 @@ ret:
- positionResp: contains the result of the decrease position and the new position
- err: error
*/
// TODO(https://github.com/NibiruChain/nibiru/issues/403): implement fluctuation limit check
func (k Keeper) decreasePosition(
ctx sdk.Context,
currentPosition types.Position,
decreasedNotional sdk.Dec,
baseAmtLimit sdk.Dec,
skipFluctuationLimitCheck bool,
) (positionResp *types.PositionResp, err error) {
if currentPosition.Size_.IsZero() {
return nil, fmt.Errorf("current position size is zero, nothing to decrease")
}

positionResp = &types.PositionResp{
RealizedPnl: sdk.ZeroDec(),
MarginToVault: sdk.ZeroDec(),
}

Expand Down Expand Up @@ -393,12 +393,10 @@ func (k Keeper) decreasePosition(
return nil, err
}

if !currentPosition.Size_.IsZero() {
positionResp.RealizedPnl = currentUnrealizedPnL.Mul(
positionResp.ExchangedPositionSize.Abs().
Quo(currentPosition.Size_.Abs()),
)
}
positionResp.RealizedPnl = currentUnrealizedPnL.Mul(
positionResp.ExchangedPositionSize.Abs().
Quo(currentPosition.Size_.Abs()),
)

remaining, err := k.CalcRemainMarginWithFundingPayment(
ctx,
Expand Down Expand Up @@ -455,6 +453,7 @@ args:
- quoteAssetAmount: the amount of notional value to move by. Must be greater than the existingPosition's notional value.
- leverage: the amount of leverage to take
- baseAmtLimit: limit on the base asset movement to ensure trader doesn't get screwed
- skipFluctuationLimitCheck: whether or not to skip the fluctuation limit check

ret:
- positionResp: response object containing information about the position change
Expand All @@ -475,7 +474,8 @@ func (k Keeper) closeAndOpenReversePosition(
closePositionResp, err := k.closePositionEntirely(
ctx,
existingPosition,
sdk.ZeroDec(),
/* quoteAssetAmountLimit */ sdk.ZeroDec(),
/* skipFluctuationLimitCheck */ false,
)
if err != nil {
return nil, err
Expand All @@ -495,12 +495,11 @@ func (k Keeper) closeAndOpenReversePosition(
"provided quote asset amount and leverage not large enough to close position. need %s but got %s",
closePositionResp.ExchangedNotionalValue.String(), reverseNotionalValue.String())
} else if remainingReverseNotionalValue.IsPositive() {
updatedbaseAmtLimit := baseAmtLimit
updatedBaseAmtLimit := baseAmtLimit
if baseAmtLimit.IsPositive() {
updatedbaseAmtLimit = baseAmtLimit.
Sub(closePositionResp.ExchangedPositionSize.Abs())
updatedBaseAmtLimit = baseAmtLimit.Sub(closePositionResp.ExchangedPositionSize.Abs())
}
if updatedbaseAmtLimit.IsNegative() {
if updatedBaseAmtLimit.IsNegative() {
return nil, fmt.Errorf(
"position size changed by greater than the specified base limit: %s",
baseAmtLimit.String(),
Expand All @@ -525,12 +524,13 @@ func (k Keeper) closeAndOpenReversePosition(
*newPosition,
sideToTake,
remainingReverseNotionalValue,
updatedbaseAmtLimit,
updatedBaseAmtLimit,
leverage,
)
if err != nil {
return nil, err
}

positionResp = &types.PositionResp{
Position: increasePositionResp.Position,
PositionNotional: increasePositionResp.PositionNotional,
Expand Down Expand Up @@ -558,6 +558,7 @@ args:
- ctx: cosmos-sdk context
- currentPosition: current position
- quoteAssetAmountLimit: a limit on quote asset to ensure trader doesn't get screwed
- skipFluctuationLimitCheck: whether or not to skip the fluctuation limit check

ret:
- positionResp: response object containing information about the position change
Expand All @@ -567,6 +568,7 @@ func (k Keeper) closePositionEntirely(
ctx sdk.Context,
currentPosition types.Position,
quoteAssetAmountLimit sdk.Dec,
skipFluctuationLimitCheck bool,
) (positionResp *types.PositionResp, err error) {
if currentPosition.Size_.IsZero() {
return nil, fmt.Errorf("zero position size")
Expand Down Expand Up @@ -618,7 +620,7 @@ func (k Keeper) closePositionEntirely(
baseAssetDirection,
currentPosition.Size_.Abs(),
quoteAssetAmountLimit,
false,
skipFluctuationLimitCheck,
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -664,7 +666,12 @@ func (k Keeper) ClosePosition(ctx sdk.Context, pair common.AssetPair, traderAddr
return nil, err
}

positionResp, err := k.closePositionEntirely(ctx, *position, sdk.ZeroDec())
positionResp, err := k.closePositionEntirely(
ctx,
*position,
/* quoteAssetAmountLimit */ sdk.ZeroDec(),
/* skipFluctuationLimitCheck */ false,
)
if err != nil {
return nil, err
}
Expand Down
60 changes: 56 additions & 4 deletions x/perp/keeper/clearing_house_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,22 @@ func TestOpenPositionSuccess(t *testing.T) {
expectedRealizedPnl: sdk.MustNewDecFromStr("-0.000099999999"),
expectedMarginToVault: sdk.MustNewDecFromStr("1000.0001099999989"),
},
{
name: "new long position just under fluctuation limit",
traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1_000_000_000_000)),
initialPosition: nil,
side: types.Side_BUY,
margin: sdk.NewInt(47_619_047_619),
leverage: sdk.OneDec(),
baseLimit: sdk.ZeroDec(),
expectedMargin: sdk.NewDec(47_619_047_619),
expectedOpenNotional: sdk.NewDec(47_619_047_619),
expectedSize: sdk.MustNewDecFromStr("45454545454.502066115702477367"),
expectedPositionNotional: sdk.NewDec(47_619_047_619),
expectedUnrealizedPnl: sdk.ZeroDec(),
expectedRealizedPnl: sdk.ZeroDec(),
expectedMarginToVault: sdk.NewDec(47_619_047_619),
},
{
name: "new short position",
traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1020)),
Expand Down Expand Up @@ -208,6 +224,22 @@ func TestOpenPositionSuccess(t *testing.T) {
expectedRealizedPnl: sdk.MustNewDecFromStr("-0.000100000001"),
expectedMarginToVault: sdk.MustNewDecFromStr("1000.0000900000009"),
},
{
name: "new short position just under fluctuation limit",
traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1_000_000_000_000)),
initialPosition: nil,
side: types.Side_SELL,
margin: sdk.NewInt(47_619_047_619),
leverage: sdk.OneDec(),
baseLimit: sdk.ZeroDec(),
expectedMargin: sdk.NewDec(47_619_047_619),
expectedOpenNotional: sdk.NewDec(47_619_047_619),
expectedSize: sdk.MustNewDecFromStr("-49999999999.947500000000002625"),
expectedPositionNotional: sdk.NewDec(47_619_047_619),
expectedUnrealizedPnl: sdk.ZeroDec(),
expectedRealizedPnl: sdk.ZeroDec(),
expectedMarginToVault: sdk.NewDec(47_619_047_619),
},
}

for _, testCase := range testCases {
Expand All @@ -232,7 +264,7 @@ func TestOpenPositionSuccess(t *testing.T) {
/* tradeLimitRatio */ sdk.OneDec(),
/* quoteReserve */ sdk.NewDec(1_000_000_000_000),
/* baseReserve */ sdk.NewDec(1_000_000_000_000),
/* fluctuationLimit */ sdk.OneDec(),
/* fluctuationLimit */ sdk.MustNewDecFromStr("0.1"),
/* maxOracleSpreadRatio */ sdk.OneDec(),
/* maintenanceMarginRatio */ sdk.MustNewDecFromStr("0.0625"),
)
Expand Down Expand Up @@ -315,15 +347,15 @@ func TestOpenPositionError(t *testing.T) {
traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 999)),
initialPosition: &types.Position{
Pair: common.PairBTCStable,
Size_: sdk.NewDec(1),
Size_: sdk.OneDec(),
Margin: sdk.NewDec(1000),
OpenNotional: sdk.NewDec(10_000),
LastUpdateCumulativePremiumFraction: sdk.ZeroDec(),
BlockNumber: 1,
},
side: types.Side_BUY,
margin: sdk.NewInt(1),
leverage: sdk.NewDec(1),
leverage: sdk.OneDec(),
baseLimit: sdk.ZeroDec(),
expectedErr: fmt.Errorf("margin ratio did not meet criteria"),
},
Expand Down Expand Up @@ -367,6 +399,26 @@ func TestOpenPositionError(t *testing.T) {
baseLimit: sdk.NewDec(10_000),
expectedErr: types.ErrLeverageIsZero,
},
{
name: "new long position over fluctuation limit",
traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1_000_000_000_000)),
initialPosition: nil,
side: types.Side_BUY,
margin: sdk.NewInt(100_000_000_000),
leverage: sdk.OneDec(),
baseLimit: sdk.ZeroDec(),
expectedErr: vpooltypes.ErrOverFluctuationLimit,
},
{
name: "new short position over fluctuation limit",
traderFunds: sdk.NewCoins(sdk.NewInt64Coin(common.DenomStable, 1_000_000_000_000)),
initialPosition: nil,
side: types.Side_SELL,
margin: sdk.NewInt(100_000_000_000),
leverage: sdk.OneDec(),
baseLimit: sdk.ZeroDec(),
expectedErr: vpooltypes.ErrOverFluctuationLimit,
},
}

for _, testCase := range testCases {
Expand All @@ -390,7 +442,7 @@ func TestOpenPositionError(t *testing.T) {
/* tradeLimitRatio */ sdk.OneDec(),
/* quoteReserve */ sdk.NewDec(1_000_000_000_000),
/* baseReserve */ sdk.NewDec(1_000_000_000_000),
/* fluctuationLimit */ sdk.OneDec(),
/* fluctuationLimit */ sdk.MustNewDecFromStr("0.1"),
/* maxOracleSpreadRatio */ sdk.OneDec(),
/* maintenanceMarginRatio */ sdk.MustNewDecFromStr("0.0625"),
)
Expand Down
1 change: 1 addition & 0 deletions x/perp/keeper/clearing_house_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,7 @@ func TestClosePositionEntirely(t *testing.T) {
ctx,
tc.initialPosition,
/*quoteAssetLimit=*/ tc.quoteAssetLimit, // NUSD
/* skipFluctuationLimitCheck */ false,
)

require.NoError(t, err)
Expand Down
7 changes: 4 additions & 3 deletions x/perp/keeper/liquidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,9 @@ func (k Keeper) ExecuteFullLiquidation(
positionResp, err := k.closePositionEntirely(
ctx,
/* currentPosition */ *position,
/* quoteAssetAmountLimit */ sdk.ZeroDec())
/* quoteAssetAmountLimit */ sdk.ZeroDec(),
/* skipFluctuationLimitCheck */ true,
)
if err != nil {
return types.LiquidateResp{}, err
}
Expand Down Expand Up @@ -277,11 +279,10 @@ func (k Keeper) ExecutePartialLiquidation(
return types.LiquidateResp{}, err
}

positionResp, err := k.openReversePosition(
positionResp, err := k.decreasePosition(
/* ctx */ ctx,
/* currentPosition */ *currentPosition,
/* quoteAssetAmount */ partiallyLiquidatedPositionNotional,
/* leverage */ sdk.OneDec(),
/* baseAmtLimit */ sdk.ZeroDec(),
/* skipFluctuationLimitCheck */ true,
)
Expand Down
8 changes: 4 additions & 4 deletions x/perp/keeper/liquidate_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func TestLiquidateIntoPartialLiquidation(t *testing.T) {
vpooltypes.Direction_ADD_TO_POOL,
sdk.OneDec(),
).
Return(tc.newPositionNotional, nil).Times(4)
Return(tc.newPositionNotional, nil).Times(3)
mocks.mockVpoolKeeper.EXPECT().
GetBaseAssetPrice(
ctx,
Expand Down Expand Up @@ -311,7 +311,7 @@ func TestLiquidateIntoFullLiquidation(t *testing.T) {
vpooltypes.Direction_ADD_TO_POOL,
/* baseAmt */ tc.initialPositionSize,
/* quoteLimit */ sdk.ZeroDec(),
/* skipFluctuationLimitCheck */ false,
/* skipFluctuationLimitCheck */ true,
).
Return(tc.newPositionNotional, nil)

Expand Down Expand Up @@ -480,7 +480,7 @@ func TestLiquidateIntoFullLiquidationWithBadDebt(t *testing.T) {
vpooltypes.Direction_ADD_TO_POOL,
/* baseAmt */ tc.initialPositionSize,
/* quoteLimit */ sdk.ZeroDec(),
/* skipFluctuationLimitCheck */ false,
/* skipFluctuationLimitCheck */ true,
).
Return(tc.newPositionNotional, nil)

Expand Down Expand Up @@ -943,7 +943,7 @@ func TestKeeper_ExecuteFullLiquidation(t *testing.T) {
baseAssetDirection,
/*baseAssetAmount=*/ tc.initialPositionSize.Abs(),
/*quoteAssetAssetLimit=*/ sdk.ZeroDec(),
/* skipFluctuationLimitCheck */ false,
/* skipFluctuationLimitCheck */ true,
).Return( /*quoteAssetAmount=*/ tc.baseAssetPriceInQuote, nil)
mocks.mockVpoolKeeper.EXPECT().
GetSpotPrice(ctx, common.PairBTCStable).
Expand Down
6 changes: 3 additions & 3 deletions x/vpool/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,7 @@ func (k Keeper) SwapQuoteForBase(
return sdk.Dec{}, err
}

if dir == types.Direction_REMOVE_FROM_POOL &&
!pool.HasEnoughQuoteReserve(quoteAssetAmount) {
if dir == types.Direction_REMOVE_FROM_POOL && !pool.HasEnoughQuoteReserve(quoteAssetAmount) {
return sdk.Dec{}, types.ErrOverTradingLimit
}

Expand All @@ -181,6 +180,7 @@ func (k Keeper) SwapQuoteForBase(
return sdk.Dec{}, err
}

// check if base asset limit is violated
if !baseAmountLimit.IsZero() {
// if going long and the base amount retrieved from the pool is less than the limit
if dir == types.Direction_ADD_TO_POOL && baseAssetAmount.LT(baseAmountLimit) {
Expand Down Expand Up @@ -268,7 +268,7 @@ func (k Keeper) checkFluctuationLimitRatio(ctx sdk.Context, pool *types.Pool) er
}

/**
isOverFluctuationLimit compares the updated pool's reserves with the given reserve snapshot, and errors if the fluctuation is above the bounds.
isOverFluctuationLimit compares the updated pool's spot price with the current spot price.

If the fluctuation limit ratio is zero, then the fluctuation limit check is skipped.

Expand Down