Skip to content

Commit

Permalink
Dev 1847 create stop loss triggering logic (#787)
Browse files Browse the repository at this point in the history
* feat:perpetual add stop loss price

* feat: add test

* feat: add cli command

* fix: test

* fix

* mgration script

* store borrow rate

* add borrow rate

* add funding rate DS and logic

* trigger

* handle interest

* rename

* fix

* handle funding rates

* remove

* exclude time from interest query

* add queries

* fix

* fix

* fixes

* add test

* tests

* more coverage

* add

* add message

* add close positions

* handle cli

* refactor

* last funding block

* use leveragelp algo for funding

* add migration script

* fix

* fix

* custody

* update health

* health

* add custody to history

* set distribution value

* fix

* feat: stop loss trigger

* add to pay

* update

* proto

* feat: add stop loss trigger

* update

* test

* refactor

* mig

* update

* skip

* add

* fix

* add test

---------

Co-authored-by: Amit <[email protected]>
Co-authored-by: Cosmic Vagabond <[email protected]>
  • Loading branch information
3 people authored Sep 11, 2024
1 parent b5a0597 commit f71027b
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 2 deletions.
2 changes: 1 addition & 1 deletion proto/elys/perpetual/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,4 @@ message MsgUpdateStopLoss {
string price = 3 [(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false];
}

message MsgUpdateStopLossResponse {}
message MsgUpdateStopLossResponse {}
27 changes: 26 additions & 1 deletion x/perpetual/keeper/msg_server_close_positions.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,35 @@ func (k msgServer) ClosePositions(goCtx context.Context, msg *types.MsgClosePosi
}
}

//Handle StopLoss
closeLog := []string{}
for _, val := range msg.StopLoss {
owner := sdk.MustAccAddressFromBech32(val.Address)
position, err := k.GetMTP(ctx, owner, val.Id)
if err != nil {
continue
}

pool, poolFound := k.GetPool(ctx, position.AmmPoolId)
if !poolFound {
continue
}
ammPool, poolErr := k.GetAmmPool(ctx, position.AmmPoolId, position.TradingAsset)
if poolErr != nil {
continue
}

err = k.CheckAndCloseAtStopLoss(ctx, &position, pool, ammPool, baseCurrency.Denom, baseCurrency.Decimals)
if err != nil {
// Add log about error or not closed
closeLog = append(closeLog, fmt.Sprintf("Position: Address:%s Id:%d cannot be liquidated due to err: %s", position.Address, position.Id, err.Error()))
}
}

// TODO: Handle stop loss
ctx.EventManager().EmitEvent(sdk.NewEvent(types.EventClosePositions,
sdk.NewAttribute("liquidations", strings.Join(liqLog, "\n")),
//sdk.NewAttribute("stop_loss", strings.Join(closeLog, "\n")),
sdk.NewAttribute("stop_loss", strings.Join(closeLog, "\n")),
))

return &types.MsgClosePositionsResponse{}, nil
Expand Down
39 changes: 39 additions & 0 deletions x/perpetual/keeper/process_mtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,42 @@ func (k Keeper) CheckAndLiquidateUnhealthyPosition(ctx sdk.Context, mtp *types.M

return nil
}

func (k Keeper) CheckAndCloseAtStopLoss(ctx sdk.Context, mtp *types.MTP, pool types.Pool, ammPool ammtypes.Pool, baseCurrency string, baseCurrencyDecimal uint64) error {
defer func() {
if r := recover(); r != nil {
if msg, ok := r.(string); ok {
ctx.Logger().Error(msg)
}
}
}()

lpTokenPrice, err := ammPool.LpTokenPrice(ctx, k.oracleKeeper)

This comment has been minimized.

Copy link
@amityadav0

amityadav0 Sep 11, 2024

Author Contributor

shouldn't this be triggered when custody price is below stop loss price ? we are not leveraging lp here, Please confirm with Wari

This comment has been minimized.

Copy link
@cosmic-vagabond

cosmic-vagabond Sep 11, 2024

Author Contributor

@amityadav0 good call out @cryptokage1996 can you fix that thanks

if err != nil {
return err
}

underStopLossPrice := !mtp.StopLossPrice.IsNil() && lpTokenPrice.LTE(mtp.StopLossPrice)
if !underStopLossPrice {
return fmt.Errorf("mtp stop loss price is not <= lp token price")
}

var repayAmount math.Int
switch mtp.Position {
case types.Position_LONG:
repayAmount, err = k.ForceCloseLong(ctx, mtp, &pool, true, baseCurrency)
case types.Position_SHORT:
repayAmount, err = k.ForceCloseShort(ctx, mtp, &pool, true, baseCurrency)
default:
return errors.Wrap(types.ErrInvalidPosition, fmt.Sprintf("invalid position type: %s", mtp.Position))
}

if err == nil {
// Emit event if position was closed
k.EmitForceClose(ctx, mtp, repayAmount, "")
} else {
return errors.Wrap(err, "error executing force close")
}

return nil
}
131 changes: 131 additions & 0 deletions x/perpetual/keeper/process_mtp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
ammtypes "github.com/elys-network/elys/x/amm/types"
assetprofiletypes "github.com/elys-network/elys/x/assetprofile/types"

// oracletypes "github.com/elys-network/elys/x/oracle/types"
"github.com/elys-network/elys/x/perpetual/types"

// "github.com/cometbft/cometbft/crypto/ed25519"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
simapp "github.com/elys-network/elys/app"
ptypes "github.com/elys-network/elys/x/parameter/types"
Expand Down Expand Up @@ -190,4 +193,132 @@ func TestCheckAndLiquidateUnhealthyPosition(t *testing.T) {
require.Equal(t, len(mtps), 0)
}

func TestCheckAndLiquidateStopLossPosition(t *testing.T) {
app := simapp.InitElysTestApp(true)
ctx := app.BaseApp.NewContext(true, tmproto.Header{})

mk, amm, oracle := app.PerpetualKeeper, app.AmmKeeper, app.OracleKeeper

// Setup coin prices
SetupStableCoinPrices(ctx, oracle)

// Set asset profile
app.AssetprofileKeeper.SetEntry(ctx, assetprofiletypes.Entry{
BaseDenom: ptypes.BaseCurrency,
Denom: ptypes.BaseCurrency,
Decimals: 6,
})
app.AssetprofileKeeper.SetEntry(ctx, assetprofiletypes.Entry{
BaseDenom: ptypes.ATOM,
Denom: ptypes.ATOM,
Decimals: 6,
})

// Generate 1 random account with 1000stake balanced
addr := simapp.AddTestAddrs(app, ctx, 3, sdk.NewInt(1000000000000))

// Create a pool
// Mint 100000USDC
usdcToken := []sdk.Coin{sdk.NewCoin(ptypes.BaseCurrency, sdk.NewInt(200000000000))}
// Mint 100000ATOM
atomToken := []sdk.Coin{sdk.NewCoin(ptypes.ATOM, sdk.NewInt(200000000000))}

err := app.BankKeeper.MintCoins(ctx, ammtypes.ModuleName, usdcToken)
require.NoError(t, err)
err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, ammtypes.ModuleName, addr[0], usdcToken)
require.NoError(t, err)

err = app.BankKeeper.MintCoins(ctx, ammtypes.ModuleName, atomToken)
require.NoError(t, err)
err = app.BankKeeper.SendCoinsFromModuleToAccount(ctx, ammtypes.ModuleName, addr[0], atomToken)
require.NoError(t, err)

poolAssets := []ammtypes.PoolAsset{
{
Weight: sdk.NewInt(50),
Token: sdk.NewCoin(ptypes.ATOM, sdk.NewInt(10000000000)),
},
{
Weight: sdk.NewInt(50),
Token: sdk.NewCoin(ptypes.BaseCurrency, sdk.NewInt(100000000000)),
},
}

argSwapFee := sdk.MustNewDecFromStr("0.0")
argExitFee := sdk.MustNewDecFromStr("0.0")

poolParams := &ammtypes.PoolParams{
SwapFee: argSwapFee,
ExitFee: argExitFee,
}

msg := ammtypes.NewMsgCreatePool(
addr[0].String(),
poolParams,
poolAssets,
)

// Create a ATOM+USDC pool
poolId, err := amm.CreatePool(ctx, msg)
require.NoError(t, err)
require.Equal(t, poolId, uint64(1))

pools := amm.GetAllPool(ctx)

// check length of pools
require.Equal(t, len(pools), 1)

// check block height
require.Equal(t, int64(0), ctx.BlockHeight())

pool, found := amm.GetPool(ctx, poolId)
require.Equal(t, found, true)

poolAddress := sdk.MustAccAddressFromBech32(pool.GetAddress())
require.NoError(t, err)

app.BankKeeper.SendCoins(ctx, addr[0], poolAddress, sdk.NewCoins(sdk.NewCoin("stake", sdk.NewInt(1000000))))
// Balance check before create a perpetual position
balances := app.BankKeeper.GetAllBalances(ctx, poolAddress)
require.Equal(t, balances.AmountOf(ptypes.BaseCurrency), sdk.NewInt(100000000000))
require.Equal(t, balances.AmountOf(ptypes.ATOM), sdk.NewInt(10000000000))

// Create a perpetual position open msg
msg2 := types.NewMsgOpen(
addr[0].String(),
types.Position_LONG,
sdk.NewDec(5),
ptypes.ATOM,
sdk.NewCoin(ptypes.BaseCurrency, sdk.NewInt(100000000)),
sdk.MustNewDecFromStr(types.TakeProfitPriceDefault),
sdk.NewDec(2),
)

_, err = mk.Open(ctx, msg2, false)
require.NoError(t, err)

mtps := mk.GetAllMTPs(ctx)
require.Equal(t, len(mtps), 1)

balances = app.BankKeeper.GetAllBalances(ctx, poolAddress)
require.Equal(t, balances.AmountOf(ptypes.BaseCurrency), sdk.NewInt(100100000000))
require.Equal(t, balances.AmountOf(ptypes.ATOM), sdk.NewInt(10000000000))

_, found = mk.OpenLongChecker.GetPool(ctx, pool.PoolId)
require.Equal(t, found, true)

err = mk.InvariantCheck(ctx)
require.Equal(t, err, nil)

mtp := mtps[0]

perpPool, _ := mk.GetPool(ctx, pool.PoolId)

err = mk.CheckAndCloseAtStopLoss(ctx, &mtp, perpPool, pool, ptypes.BaseCurrency, 6)
require.NoError(t, err)

mtps = mk.GetAllMTPs(ctx)
require.Equal(t, len(mtps), 0)
}

// TODO: Add funding rate tests

0 comments on commit f71027b

Please sign in to comment.