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

fix(perpetual): estimated pnl formula fix #842

Merged
merged 5 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 55 additions & 42 deletions x/perpetual/keeper/mtp.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@

pendingBorrowInterest := k.GetBorrowInterest(ctx, &mtp, ammPool)
mtp.BorrowInterestUnpaidCollateral = mtp.BorrowInterestUnpaidCollateral.Add(pendingBorrowInterest)
pnl = k.GetPnL(ctx, mtp, ammPool, baseCurrency)
pnl, err = k.GetPnL(ctx, mtp, ammPool, baseCurrency)
if err != nil {
return err

Check warning on line 174 in x/perpetual/keeper/mtp.go

View check run for this annotation

Codecov / codecov/patch

x/perpetual/keeper/mtp.go#L172-L174

Added lines #L172 - L174 were not covered by tests
}
}

info, found := k.oracleKeeper.GetAssetInfo(ctx, mtp.TradingAsset)
Expand Down Expand Up @@ -232,7 +235,10 @@

pendingBorrowInterest := k.GetBorrowInterest(ctx, &mtp, ammPool)
mtp.BorrowInterestUnpaidCollateral = mtp.BorrowInterestUnpaidCollateral.Add(pendingBorrowInterest)
pnl = k.GetPnL(ctx, mtp, ammPool, baseCurrency)
pnl, err = k.GetPnL(ctx, mtp, ammPool, baseCurrency)
if err != nil {
return false, err

Check warning on line 240 in x/perpetual/keeper/mtp.go

View check run for this annotation

Codecov / codecov/patch

x/perpetual/keeper/mtp.go#L238-L240

Added lines #L238 - L240 were not covered by tests
}
}

info, found := k.oracleKeeper.GetAssetInfo(ctx, mtp.TradingAsset)
Expand Down Expand Up @@ -316,7 +322,10 @@

pendingBorrowInterest := k.GetBorrowInterest(ctx, &mtp, ammPool)
mtp.BorrowInterestUnpaidCollateral = mtp.BorrowInterestUnpaidCollateral.Add(pendingBorrowInterest)
pnl = k.GetPnL(ctx, mtp, ammPool, baseCurrency)
pnl, err = k.GetPnL(ctx, mtp, ammPool, baseCurrency)
if err != nil {
return err

Check warning on line 327 in x/perpetual/keeper/mtp.go

View check run for this annotation

Codecov / codecov/patch

x/perpetual/keeper/mtp.go#L327

Added line #L327 was not covered by tests
}
}

info, found := k.oracleKeeper.GetAssetInfo(ctx, mtp.TradingAsset)
Expand Down Expand Up @@ -412,40 +421,8 @@
return nil
}

func (k Keeper) GetPnL(ctx sdk.Context, mtp types.MTP, ammPool ammtypes.Pool, baseCurrency string) sdk.Dec {
func (k Keeper) GetPnL(ctx sdk.Context, mtp types.MTP, ammPool ammtypes.Pool, baseCurrency string) (sdk.Dec, error) {
// P&L = Custody (in USD) - Liability ( in USD) - Collateral ( in USD)
// Liability should include margin interest and funding fee accrued.
totalLiability := mtp.Liabilities

pendingBorrowInterest := k.GetBorrowInterest(ctx, &mtp, ammPool)
mtp.BorrowInterestUnpaidCollateral = mtp.BorrowInterestUnpaidCollateral.Add(pendingBorrowInterest)

// if short position, convert liabilities to base currency
if mtp.Position == types.Position_SHORT {
liabilities := sdk.NewCoin(mtp.LiabilitiesAsset, totalLiability)
var err error
totalLiability, err = k.EstimateSwapGivenOut(ctx, liabilities, baseCurrency, ammPool)
if err != nil {
totalLiability = sdk.ZeroInt()
}
}

collateral := mtp.Collateral.Add(mtp.BorrowInterestUnpaidCollateral)
// include unpaid borrow interest in debt
if collateral.IsPositive() {
unpaidCollateral := sdk.NewCoin(mtp.CollateralAsset, collateral)

if mtp.CollateralAsset == baseCurrency {
totalLiability = totalLiability.Add(collateral)
} else {
C, err := k.EstimateSwapGivenOut(ctx, unpaidCollateral, baseCurrency, ammPool)
if err != nil {
C = sdk.ZeroInt()
}

totalLiability = totalLiability.Add(C)
}
}

// Funding rate payment consideration
// get funding rate
Expand All @@ -461,19 +438,55 @@
takeAmountCustodyAmount = types.CalcTakeAmount(mtp.Custody, fundingRate)
}

pendingBorrowInterest := k.GetBorrowInterest(ctx, &mtp, ammPool)
mtp.BorrowInterestUnpaidCollateral = mtp.BorrowInterestUnpaidCollateral.Add(pendingBorrowInterest)

// Liability should include margin interest and funding fee accrued.
collateralAmt := mtp.Collateral.Add(mtp.BorrowInterestUnpaidCollateral)

// if short position, custody asset is already in base currency
custodyAmtInBaseCurrency := mtp.Custody.Sub(takeAmountCustodyAmount)

if mtp.Position == types.Position_LONG {
custodyAmt := sdk.NewCoin(mtp.CustodyAsset, mtp.Custody)
var err error
custodyAmtInBaseCurrency, err = k.EstimateSwapGivenOut(ctx, custodyAmt, baseCurrency, ammPool)
// Calculate estimated PnL
var estimatedPnL sdk.Dec

if mtp.Position == types.Position_SHORT {
// Estimated PnL for short position:
// estimated_pnl = custody_amount - liabilities_amount * market_price - collateral_amount

// For short position, convert liabilities to base currency
C, err := k.EstimateSwapGivenOut(ctx, sdk.NewCoin(mtp.LiabilitiesAsset, mtp.Liabilities), baseCurrency, ammPool)
if err != nil {
custodyAmtInBaseCurrency = sdk.ZeroInt()
return sdk.ZeroDec(), err

Check warning on line 460 in x/perpetual/keeper/mtp.go

View check run for this annotation

Codecov / codecov/patch

x/perpetual/keeper/mtp.go#L460

Added line #L460 was not covered by tests
}

estimatedPnL = custodyAmtInBaseCurrency.ToLegacyDec().Sub(C.ToLegacyDec()).Sub(collateralAmt.ToLegacyDec())
} else {
// Estimated PnL for long position:
if mtp.CollateralAsset != baseCurrency {
// estimated_pnl = (custody_amount - collateral_amount) * market_price - liabilities_amount

// For long position, convert both custody and collateral to base currency
C, err := k.EstimateSwapGivenOut(ctx, sdk.NewCoin(mtp.CollateralAsset, custodyAmtInBaseCurrency.Sub(collateralAmt)), baseCurrency, ammPool)
if err != nil {
return sdk.ZeroDec(), err

Check warning on line 472 in x/perpetual/keeper/mtp.go

View check run for this annotation

Codecov / codecov/patch

x/perpetual/keeper/mtp.go#L470-L472

Added lines #L470 - L472 were not covered by tests
}

estimatedPnL = C.ToLegacyDec().Sub(mtp.Liabilities.ToLegacyDec())

Check warning on line 475 in x/perpetual/keeper/mtp.go

View check run for this annotation

Codecov / codecov/patch

x/perpetual/keeper/mtp.go#L475

Added line #L475 was not covered by tests
} else {
// estimated_pnl = custody_amount * market_price - liabilities_amount - collateral_amount

// For long position, convert custody to base currency
C, err := k.EstimateSwapGivenOut(ctx, sdk.NewCoin(mtp.CustodyAsset, custodyAmtInBaseCurrency), baseCurrency, ammPool)
if err != nil {
return sdk.ZeroDec(), err

Check warning on line 482 in x/perpetual/keeper/mtp.go

View check run for this annotation

Codecov / codecov/patch

x/perpetual/keeper/mtp.go#L482

Added line #L482 was not covered by tests
}

estimatedPnL = C.ToLegacyDec().Sub(mtp.Liabilities.ToLegacyDec()).Sub(collateralAmt.ToLegacyDec())
}
}

return custodyAmtInBaseCurrency.ToLegacyDec().Sub(totalLiability.ToLegacyDec())
return estimatedPnL, nil
}

func (k Keeper) DeleteLegacyMTP(ctx sdk.Context, mtpaddress string, id uint64) error {
Expand Down
193 changes: 193 additions & 0 deletions x/perpetual/keeper/open_long_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ 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/keeper"
"github.com/elys-network/elys/x/perpetual/types"
"github.com/elys-network/elys/x/perpetual/types/mocks"
"github.com/stretchr/testify/assert"

"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 @@ -636,6 +638,197 @@ func TestOpenLong_ATOM_Collateral(t *testing.T) {
}, mtp)
}

func TestOpenLong_Long10XAtom1000Usdc(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)

provider := sdk.AccAddress(ed25519.GenPrivKey().PubKey().Address())
oracle.SetPrice(ctx, oracletypes.Price{
Asset: "USDC",
Price: sdk.NewDec(1),
Source: "elys",
Provider: provider.String(),
Timestamp: uint64(ctx.BlockTime().Unix()),
})
oracle.SetPrice(ctx, oracletypes.Price{
Asset: "ATOM",
Price: sdk.MustNewDecFromStr("4.39"),
Source: "atom",
Provider: provider.String(),
Timestamp: uint64(ctx.BlockTime().Unix()),
})
oracle.SetPrice(ctx, oracletypes.Price{
Asset: "uatom",
Price: sdk.MustNewDecFromStr("4.39"),
Source: "uatom",
Provider: provider.String(),
Timestamp: uint64(ctx.BlockTime().Unix()),
})

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

// Create a pool
// Mint 100_000_000USDC
usdcToken := []sdk.Coin{sdk.NewCoin(ptypes.BaseCurrency, sdk.NewInt(100_000_000_000000))}
// Mint 100_000_000ATOM
atomToken := []sdk.Coin{sdk.NewCoin(ptypes.ATOM, sdk.NewInt(100_000_000_000000))}

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(10_000_000_000000)),
},
{
Weight: sdk.NewInt(50),
Token: sdk.NewCoin(ptypes.BaseCurrency, sdk.NewInt(10_000_000_000000)),
},
}

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

poolParams := &ammtypes.PoolParams{
UseOracle: true,
ExternalLiquidityRatio: sdk.NewDec(2),
WeightBreakingFeeMultiplier: sdk.ZeroDec(),
WeightBreakingFeeExponent: sdk.NewDecWithPrec(0, 1), // 2.5
WeightRecoveryFeePortion: sdk.NewDecWithPrec(0, 2), // 10%
ThresholdWeightDifference: sdk.ZeroDec(),
SwapFee: argSwapFee,
ExitFee: argExitFee,
FeeDenom: ptypes.BaseCurrency,
}

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)

// Balance check before create a perpetual position
balances := app.BankKeeper.GetAllBalances(ctx, poolAddress)
require.Equal(t, balances.AmountOf(ptypes.BaseCurrency), sdk.NewInt(10_000_000_000000))
require.Equal(t, balances.AmountOf(ptypes.ATOM), sdk.NewInt(10_000_000_000000))

// Create a perpetual position open msg
msg2 := types.NewMsgOpen(
addr[0].String(),
types.Position_LONG,
sdk.NewDec(10),
ptypes.ATOM,
sdk.NewCoin(ptypes.BaseCurrency, sdk.NewInt(1_000_000000)),
sdk.MustNewDecFromStr("5.0"),
sdk.ZeroDec(),
)

_, 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(10_001_000_000000))
require.Equal(t, balances.AmountOf(ptypes.ATOM), sdk.NewInt(10_000_000_000000))

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

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

mtp := mtps[0]

// Check MTP
require.Equal(t, types.MTP{
Address: addr[0].String(),
CollateralAsset: ptypes.BaseCurrency,
TradingAsset: ptypes.ATOM,
LiabilitiesAsset: ptypes.BaseCurrency,
CustodyAsset: ptypes.ATOM,
Collateral: sdk.NewInt(1_000_000000),
Liabilities: sdk.NewInt(9_000_000000),
BorrowInterestPaidCollateral: sdk.NewInt(0),
BorrowInterestPaidCustody: sdk.NewInt(0),
BorrowInterestUnpaidCollateral: sdk.NewInt(0),
Custody: sdk.NewInt(2_276_506970),
TakeProfitLiabilities: sdk.NewInt(7_898_168205),
TakeProfitCustody: sdk.NewInt(1_800_000000),
MtpHealth: sdk.MustNewDecFromStr("1.110494168573014992"),
Position: types.Position_LONG,
Id: uint64(1),
AmmPoolId: uint64(1),
TakeProfitPrice: sdk.MustNewDecFromStr("5.0"),
TakeProfitBorrowRate: sdk.MustNewDecFromStr("1.0"),
FundingFeePaidCollateral: sdk.NewInt(0),
FundingFeePaidCustody: sdk.NewInt(0),
FundingFeeReceivedCollateral: sdk.NewInt(0),
FundingFeeReceivedCustody: sdk.NewInt(0),
OpenPrice: sdk.MustNewDecFromStr("4.392694655356139762"),
StopLossPrice: sdk.ZeroDec(),
}, mtp)

oracle.SetPrice(ctx, oracletypes.Price{
Asset: "USDC",
Price: sdk.NewDec(1),
Source: "elys",
Provider: provider.String(),
Timestamp: uint64(ctx.BlockTime().Unix()),
})
oracle.SetPrice(ctx, oracletypes.Price{
Asset: "ATOM",
Price: sdk.MustNewDecFromStr("5.0"),
Source: "atom",
Provider: provider.String(),
Timestamp: uint64(ctx.BlockTime().Unix()),
})
oracle.SetPrice(ctx, oracletypes.Price{
Asset: "uatom",
Price: sdk.MustNewDecFromStr("5.0"),
Source: "uatom",
Provider: provider.String(),
Timestamp: uint64(ctx.BlockTime().Unix()),
})

resp, _, _ := mk.GetMTPsForAddressWithPagination(ctx, addr[0], nil)
require.Equal(t, resp[0].Pnl, sdk.NewDec(1_380_312708))
}

func TestOpenLongConsolidate_Success(t *testing.T) {
// Setup the mock checker
mockChecker := new(mocks.OpenDefineAssetsChecker)
Expand Down
23 changes: 15 additions & 8 deletions x/perpetual/keeper/query_open_estimation.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,23 @@ func (k Keeper) OpenEstimation(goCtx context.Context, req *types.QueryOpenEstima
liabilitiesAmountDec := sdk.NewDecFromBigInt(collateralAmountInBaseCurrency.Amount.BigInt()).Mul(req.Leverage.Sub(sdk.OneDec()))

// calculate estimated pnl
// estimated_pnl = custody_amount - (liability_amount + collateral_amount) / take_profit_price
estimatedPnL := sdk.NewDecFromBigInt(positionSize.Amount.BigInt())
estimatedPnL = estimatedPnL.Sub(liabilitiesAmountDec.Add(sdk.NewDecFromBigInt(req.Collateral.Amount.BigInt())).Quo(req.TakeProfitPrice))
estimatedPnLDenom := req.TradingAsset
var estimatedPnL sdk.Dec
estimatedPnLDenom := baseCurrency

// if position is short then estimated pnl is custody_amount / open_price - (liability_amount + collateral_amount) / take_profit_price
// if position is short then:
if req.Position == types.Position_SHORT {
estimatedPnL = liabilitiesAmountDec.Add(sdk.NewDecFromBigInt(req.Collateral.Amount.BigInt())).Quo(req.TakeProfitPrice)
estimatedPnL = estimatedPnL.Sub(sdk.NewDecFromBigInt(positionSize.Amount.BigInt()).Quo(openPrice))
estimatedPnLDenom = baseCurrency
// estimated_pnl = custody_amount - liabilities_amount * take_profit_price - collateral_amount
estimatedPnL = sdk.NewDecFromBigInt(positionSize.Amount.BigInt()).Sub(liabilitiesAmountDec.Mul(req.TakeProfitPrice)).Sub(sdk.NewDecFromBigInt(req.Collateral.Amount.BigInt()))
} else {
// if position is long then:
// if collateral is not in base currency
if req.Collateral.Denom != baseCurrency {
// estimated_pnl = (custody_amount - collateral_amount) * take_profit_price - liabilities_amount
estimatedPnL = sdk.NewDecFromBigInt(positionSize.Amount.BigInt()).Sub(sdk.NewDecFromBigInt(req.Collateral.Amount.BigInt())).Mul(req.TakeProfitPrice).Sub(liabilitiesAmountDec)
} else {
// estimated_pnl = custody_amount * take_profit_price - liabilities_amount - collateral_amount
estimatedPnL = sdk.NewDecFromBigInt(positionSize.Amount.BigInt()).Mul(req.TakeProfitPrice).Sub(liabilitiesAmountDec).Sub(sdk.NewDecFromBigInt(req.Collateral.Amount.BigInt()))
}
}

// calculate liquidation price
Expand Down
Loading
Loading