github.com/InjectiveLabs/sdk-go@v1.53.0/chain/exchange/types/positions.go (about)

     1  package types
     2  
     3  import (
     4  	"cosmossdk.io/math"
     5  )
     6  
     7  type positionPayout struct {
     8  	Payout       math.LegacyDec
     9  	PnlNotional  math.LegacyDec
    10  	IsProfitable bool
    11  }
    12  
    13  func (p *Position) IsShort() bool { return !p.IsLong }
    14  
    15  func (p *Position) Copy() *Position {
    16  	return &Position{
    17  		IsLong:                 p.IsLong,
    18  		Quantity:               p.Quantity,
    19  		EntryPrice:             p.EntryPrice,
    20  		Margin:                 p.Margin,
    21  		CumulativeFundingEntry: p.CumulativeFundingEntry,
    22  	}
    23  }
    24  
    25  func (p *DerivativePosition) Copy() *DerivativePosition {
    26  	return &DerivativePosition{
    27  		SubaccountId: p.SubaccountId,
    28  		MarketId:     p.MarketId,
    29  		Position:     p.Position.Copy(),
    30  	}
    31  }
    32  
    33  func (m *PositionDelta) IsShort() bool { return !m.IsLong }
    34  
    35  // NewPosition initializes a new position with a given cumulativeFundingEntry (should be nil for non-perpetual markets)
    36  func NewPosition(isLong bool, cumulativeFundingEntry math.LegacyDec) *Position {
    37  	position := &Position{
    38  		IsLong:     isLong,
    39  		Quantity:   math.LegacyZeroDec(),
    40  		EntryPrice: math.LegacyZeroDec(),
    41  		Margin:     math.LegacyZeroDec(),
    42  	}
    43  	if !cumulativeFundingEntry.IsNil() {
    44  		position.CumulativeFundingEntry = cumulativeFundingEntry
    45  	}
    46  	return position
    47  }
    48  
    49  // GetEffectiveMarginRatio returns the effective margin ratio of the position, based on the input closing price.
    50  // CONTRACT: position must already be funding-adjusted (if perpetual) and have positive quantity.
    51  func (p *Position) GetEffectiveMarginRatio(closingPrice, closingFee math.LegacyDec) (marginRatio math.LegacyDec) {
    52  	// nolint:all
    53  	// marginRatio = (margin + quantity * PnlPerContract) / (closingPrice * quantity)
    54  	effectiveMargin := p.Margin.Add(p.GetPayoutFromPnl(closingPrice, p.Quantity)).Sub(closingFee)
    55  	return effectiveMargin.Quo(closingPrice.Mul(p.Quantity))
    56  }
    57  
    58  // ApplyProfitHaircutForDerivatives results in reducing the payout (pnl * quantity) by the given rate (e.g. 0.1=10%) by modifying the entry price.
    59  // Formula for adjustment:
    60  // newPayoutFromPnl = oldPayoutFromPnl * (1 - missingFundsRate)
    61  // => Entry price adjustment for buys
    62  // (newEntryPrice - settlementPrice) * quantity = (entryPrice - settlementPrice) * quantity * (1 - missingFundsRate)
    63  // newEntryPrice = entryPrice - entryPrice * haircutPercentage + settlementPrice * haircutPercentage
    64  // => Entry price adjustment for sells
    65  // (settlementPrice - newEntryPrice) * quantity = (settlementPrice - entryPrice) * quantity * (1 - missingFundsRate)
    66  // newEntryPrice = entryPrice - entryPrice * haircutPercentage + settlementPrice * haircutPercentage
    67  func (p *Position) ApplyProfitHaircutForDerivatives(deficitAmount, totalProfits, settlementPrice math.LegacyDec) {
    68  	// haircutPercentage = deficitAmount / totalProfits
    69  	// To preserve precision, the division by totalProfits is done last.
    70  	// newEntryPrice =  haircutPercentage * (settlementPrice - entryPrice) + entryPrice
    71  	newEntryPrice := deficitAmount.Mul(settlementPrice.Sub(p.EntryPrice)).Quo(totalProfits).Add(p.EntryPrice)
    72  	p.EntryPrice = newEntryPrice
    73  
    74  	// profitable position but with negative margin, we didn't account for negative margin previously,
    75  	// so we can safely add it if payout becomes negative from haircut
    76  	newPositionPayout := p.GetPayoutIfFullyClosing(settlementPrice, math.LegacyZeroDec()).Payout
    77  	if newPositionPayout.IsNegative() {
    78  		p.Margin = p.Margin.Add(newPositionPayout.Abs())
    79  	}
    80  }
    81  
    82  func (p *Position) ApplyTotalPositionPayoutHaircut(deficitAmount, totalPayouts, settlementPrice math.LegacyDec) {
    83  	p.ApplyProfitHaircutForDerivatives(deficitAmount, totalPayouts, settlementPrice)
    84  
    85  	removedMargin := p.Margin.Mul(deficitAmount).Quo(totalPayouts)
    86  	p.Margin = p.Margin.Sub(removedMargin)
    87  }
    88  
    89  func (p *Position) ApplyProfitHaircutForBinaryOptions(deficitAmount, totalAssets math.LegacyDec, oracleScaleFactor uint32) {
    90  	// haircutPercentage = deficitAmount / totalAssets
    91  	// To preserve precision, the division by totalAssets is done last.
    92  	// newMargin =  p.Margin - p.Margin * haircutPercentage
    93  	newMargin := p.Margin.Sub(deficitAmount.Mul(p.Margin).Quo(totalAssets))
    94  	p.Margin = newMargin
    95  
    96  	// updating entry price just for consistency, but it has no effect since applied haircut is on margin, not on entry price during binary options refunds
    97  	if p.IsLong {
    98  		p.EntryPrice = p.Margin.Quo(p.Quantity)
    99  	} else {
   100  		scaledOne := GetScaledPrice(math.LegacyOneDec(), oracleScaleFactor)
   101  		p.EntryPrice = scaledOne.Sub(p.Margin.Quo(p.Quantity))
   102  	}
   103  }
   104  
   105  func (p *Position) ClosePositionWithSettlePrice(settlementPrice, closingFeeRate math.LegacyDec) (payout, closeTradingFee math.LegacyDec, positionDelta *PositionDelta, pnl math.LegacyDec) {
   106  	closingDirection := !p.IsLong
   107  	fullyClosingQuantity := p.Quantity
   108  
   109  	closeTradingFee = settlementPrice.Mul(fullyClosingQuantity).Mul(closingFeeRate)
   110  	positionDelta = &PositionDelta{
   111  		IsLong:            closingDirection,
   112  		ExecutionQuantity: fullyClosingQuantity,
   113  		ExecutionMargin:   math.LegacyZeroDec(),
   114  		ExecutionPrice:    settlementPrice,
   115  	}
   116  
   117  	// there should not be positions with 0 quantity
   118  	if fullyClosingQuantity.IsZero() {
   119  		return math.LegacyZeroDec(), closeTradingFee, positionDelta, math.LegacyZeroDec()
   120  	}
   121  
   122  	payout, _, _, pnl = p.ApplyPositionDelta(positionDelta, closeTradingFee)
   123  
   124  	return payout, closeTradingFee, positionDelta, pnl
   125  }
   126  
   127  func (p *Position) ClosePositionWithoutPayouts() {
   128  	p.IsLong = false
   129  	p.EntryPrice = math.LegacyZeroDec()
   130  	p.Quantity = math.LegacyZeroDec()
   131  	p.Margin = math.LegacyZeroDec()
   132  	p.CumulativeFundingEntry = math.LegacyZeroDec()
   133  }
   134  
   135  func (p *Position) ClosePositionByRefunding(closingFeeRate math.LegacyDec) (payout, closeTradingFee math.LegacyDec, positionDelta *PositionDelta, pnl math.LegacyDec) {
   136  	return p.ClosePositionWithSettlePrice(p.EntryPrice, closingFeeRate)
   137  }
   138  
   139  func (p *Position) GetDirectionString() string {
   140  	directionStr := "Long"
   141  	if p.IsShort() {
   142  		directionStr = "Short"
   143  	}
   144  	return directionStr
   145  }
   146  
   147  func (p *Position) CheckValidPositionToReduce(
   148  	marketType MarketType,
   149  	reducePrice math.LegacyDec,
   150  	isBuyOrder bool,
   151  	tradeFeeRate math.LegacyDec,
   152  	funding *PerpetualMarketFunding,
   153  	orderMargin math.LegacyDec,
   154  ) error {
   155  	if isBuyOrder == p.IsLong {
   156  		return ErrInvalidReduceOnlyPositionDirection
   157  	}
   158  
   159  	if marketType == MarketType_BinaryOption {
   160  		return nil
   161  	}
   162  
   163  	if err := p.checkValidClosingPrice(reducePrice, tradeFeeRate, funding, orderMargin); err != nil {
   164  		return err
   165  	}
   166  
   167  	return nil
   168  }
   169  
   170  func (p *Position) checkValidClosingPrice(closingPrice, tradeFeeRate math.LegacyDec, funding *PerpetualMarketFunding, orderMargin math.LegacyDec) error {
   171  	bankruptcyPrice := p.GetBankruptcyPriceWithAddedMargin(funding, orderMargin)
   172  
   173  	if p.IsLong {
   174  		// For long positions, Price ≥ BankruptcyPrice / (1 - TradeFeeRate) must hold
   175  		feeAdjustedBankruptcyPrice := bankruptcyPrice.Quo(math.LegacyOneDec().Sub(tradeFeeRate))
   176  
   177  		if closingPrice.LT(feeAdjustedBankruptcyPrice) {
   178  			return ErrPriceSurpassesBankruptcyPrice
   179  		}
   180  	} else {
   181  		// For short positions, Price ≤ BankruptcyPrice / (1 + TradeFeeRate) must hold
   182  		feeAdjustedBankruptcyPrice := bankruptcyPrice.Quo(math.LegacyOneDec().Add(tradeFeeRate))
   183  
   184  		if closingPrice.GT(feeAdjustedBankruptcyPrice) {
   185  			return ErrPriceSurpassesBankruptcyPrice
   186  		}
   187  	}
   188  	return nil
   189  }
   190  
   191  func (p *Position) GetLiquidationMarketOrderWorstPrice(markPrice math.LegacyDec, funding *PerpetualMarketFunding) math.LegacyDec {
   192  	bankruptcyPrice := p.GetBankruptcyPrice(funding)
   193  	hasNegativeEquity := (p.IsLong && markPrice.LT(bankruptcyPrice)) || (p.IsShort() && markPrice.GT(bankruptcyPrice))
   194  
   195  	if hasNegativeEquity {
   196  		return markPrice
   197  	}
   198  
   199  	return bankruptcyPrice
   200  }
   201  
   202  func (p *Position) GetBankruptcyPrice(funding *PerpetualMarketFunding) (bankruptcyPrice math.LegacyDec) {
   203  	return p.GetLiquidationPrice(math.LegacyZeroDec(), funding)
   204  }
   205  
   206  func (p *Position) GetBankruptcyPriceWithAddedMargin(funding *PerpetualMarketFunding, addedMargin math.LegacyDec) (bankruptcyPrice math.LegacyDec) {
   207  	return p.getLiquidationPriceWithAddedMargin(math.LegacyZeroDec(), funding, addedMargin)
   208  }
   209  
   210  func (p *Position) GetLiquidationPrice(maintenanceMarginRatio math.LegacyDec, funding *PerpetualMarketFunding) math.LegacyDec {
   211  	return p.getLiquidationPriceWithAddedMargin(maintenanceMarginRatio, funding, math.LegacyZeroDec())
   212  }
   213  
   214  func (p *Position) getLiquidationPriceWithAddedMargin(maintenanceMarginRatio math.LegacyDec, funding *PerpetualMarketFunding, addedMargin math.LegacyDec) math.LegacyDec {
   215  	adjustedUnitMargin := p.getFundingAdjustedUnitMarginWithAddedMargin(funding, addedMargin)
   216  
   217  	// TODO include closing fee for reduce only ?
   218  
   219  	var liquidationPrice math.LegacyDec
   220  	if p.IsLong {
   221  		// liquidation price = (entry price - unit margin) / (1 - maintenanceMarginRatio)
   222  		liquidationPrice = p.EntryPrice.Sub(adjustedUnitMargin).Quo(math.LegacyOneDec().Sub(maintenanceMarginRatio))
   223  	} else {
   224  		// liquidation price = (entry price + unit margin) / (1 + maintenanceMarginRatio)
   225  		liquidationPrice = p.EntryPrice.Add(adjustedUnitMargin).Quo(math.LegacyOneDec().Add(maintenanceMarginRatio))
   226  	}
   227  	return liquidationPrice
   228  }
   229  
   230  func (p *Position) GetEffectiveMargin(funding *PerpetualMarketFunding, closingPrice math.LegacyDec) math.LegacyDec {
   231  	fundingAdjustedMargin := p.Margin
   232  	if funding != nil {
   233  		fundingAdjustedMargin = p.getFundingAdjustedMargin(funding)
   234  	}
   235  	pnlNotional := math.LegacyZeroDec()
   236  	if !closingPrice.IsNil() {
   237  		pnlNotional = p.GetPayoutFromPnl(closingPrice, p.Quantity)
   238  	}
   239  	effectiveMargin := fundingAdjustedMargin.Add(pnlNotional)
   240  	return effectiveMargin
   241  }
   242  
   243  // ApplyFunding updates the position to account for any funding payment.
   244  func (p *Position) ApplyFunding(funding *PerpetualMarketFunding) {
   245  	if funding != nil {
   246  		p.Margin = p.getFundingAdjustedMargin(funding)
   247  
   248  		// update the cumulative funding entry to current
   249  		p.CumulativeFundingEntry = funding.CumulativeFunding
   250  	}
   251  }
   252  
   253  func (p *Position) getFundingAdjustedMargin(funding *PerpetualMarketFunding) math.LegacyDec {
   254  	return p.getFundingAdjustedMarginWithAddedMargin(funding, math.LegacyZeroDec())
   255  }
   256  
   257  func (p *Position) getFundingAdjustedMarginWithAddedMargin(funding *PerpetualMarketFunding, addedMargin math.LegacyDec) math.LegacyDec {
   258  	adjustedMargin := p.Margin.Add(addedMargin)
   259  
   260  	// Compute the adjusted position margin for positions in perpetual markets
   261  	if funding != nil {
   262  		unrealizedFundingPayment := p.Quantity.Mul(funding.CumulativeFunding.Sub(p.CumulativeFundingEntry))
   263  
   264  		// For longs, Margin -= Funding
   265  		// For shorts, Margin += Funding
   266  		if p.IsLong {
   267  			adjustedMargin = adjustedMargin.Sub(unrealizedFundingPayment)
   268  		} else {
   269  			adjustedMargin = adjustedMargin.Add(unrealizedFundingPayment)
   270  		}
   271  	}
   272  
   273  	return adjustedMargin
   274  }
   275  
   276  func (p *Position) getFundingAdjustedUnitMarginWithAddedMargin(funding *PerpetualMarketFunding, addedMargin math.LegacyDec) math.LegacyDec {
   277  	adjustedMargin := p.getFundingAdjustedMarginWithAddedMargin(funding, addedMargin)
   278  
   279  	// Unit Margin = PositionMargin / PositionQuantity
   280  	fundingAdjustedUnitMargin := adjustedMargin.Quo(p.Quantity)
   281  	return fundingAdjustedUnitMargin
   282  }
   283  
   284  func (p *Position) GetAverageWeightedEntryPrice(executionQuantity, executionPrice math.LegacyDec) math.LegacyDec {
   285  	num := p.Quantity.Mul(p.EntryPrice).Add(executionQuantity.Mul(executionPrice))
   286  	denom := p.Quantity.Add(executionQuantity)
   287  
   288  	return num.Quo(denom)
   289  }
   290  
   291  func (p *Position) GetPayoutIfFullyClosing(closingPrice, closingFeeRate math.LegacyDec) *positionPayout {
   292  	isProfitable := (p.IsLong && p.EntryPrice.LT(closingPrice)) || (!p.IsLong && p.EntryPrice.GT(closingPrice))
   293  
   294  	fullyClosingQuantity := p.Quantity
   295  	positionMargin := p.Margin
   296  
   297  	closeTradingFee := closingPrice.Mul(fullyClosingQuantity).Mul(closingFeeRate)
   298  	payoutFromPnl := p.GetPayoutFromPnl(closingPrice, fullyClosingQuantity)
   299  	pnlNotional := payoutFromPnl.Sub(closeTradingFee)
   300  	payout := pnlNotional.Add(positionMargin)
   301  
   302  	return &positionPayout{
   303  		Payout:       payout,
   304  		PnlNotional:  pnlNotional,
   305  		IsProfitable: isProfitable,
   306  	}
   307  }
   308  
   309  func (p *Position) GetPayoutFromPnl(closingPrice, closingQuantity math.LegacyDec) math.LegacyDec {
   310  	var pnlNotional math.LegacyDec
   311  
   312  	if p.IsLong {
   313  		// nolint:all
   314  		// pnl = closingQuantity * (executionPrice - entryPrice)
   315  		pnlNotional = closingQuantity.Mul(closingPrice.Sub(p.EntryPrice))
   316  	} else {
   317  		// nolint:all
   318  		// pnl = -closingQuantity * (executionPrice - entryPrice)
   319  		pnlNotional = closingQuantity.Mul(closingPrice.Sub(p.EntryPrice)).Neg()
   320  	}
   321  
   322  	return pnlNotional
   323  }
   324  
   325  func (p *Position) ApplyPositionDelta(delta *PositionDelta, tradingFeeForReduceOnly math.LegacyDec) (
   326  	payout, closeExecutionMargin, collateralizationMargin, pnl math.LegacyDec,
   327  ) {
   328  	// No payouts or margin changes if the position delta is nil
   329  	if delta == nil || p == nil {
   330  		return math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec()
   331  	}
   332  
   333  	if p.Quantity.IsZero() {
   334  		p.IsLong = delta.IsLong
   335  	}
   336  
   337  	payout, closeExecutionMargin, collateralizationMargin = math.LegacyZeroDec(), math.LegacyZeroDec(), math.LegacyZeroDec()
   338  	isNettingInSameDirection := (p.IsLong && delta.IsLong) || (p.IsShort() && delta.IsShort())
   339  
   340  	if isNettingInSameDirection {
   341  		p.EntryPrice = p.GetAverageWeightedEntryPrice(delta.ExecutionQuantity, delta.ExecutionPrice)
   342  		p.Quantity = p.Quantity.Add(delta.ExecutionQuantity)
   343  		p.Margin = p.Margin.Add(delta.ExecutionMargin)
   344  		collateralizationMargin = delta.ExecutionMargin
   345  
   346  		return payout, closeExecutionMargin, collateralizationMargin, math.LegacyZeroDec()
   347  	}
   348  
   349  	// netting in opposing direction
   350  	closingQuantity := math.LegacyMinDec(p.Quantity, delta.ExecutionQuantity)
   351  	// closeExecutionMargin = execution margin * closing quantity / execution quantity
   352  	closeExecutionMargin = delta.ExecutionMargin.Mul(closingQuantity).Quo(delta.ExecutionQuantity)
   353  
   354  	pnlNotional := p.GetPayoutFromPnl(delta.ExecutionPrice, closingQuantity)
   355  	isReduceOnlyTrade := delta.ExecutionMargin.IsZero()
   356  
   357  	if isReduceOnlyTrade {
   358  		// deduct fees from PNL (position margin) for reduce-only orders
   359  
   360  		// only use the closing trading fee for now
   361  		pnlNotional = pnlNotional.Sub(tradingFeeForReduceOnly)
   362  	}
   363  
   364  	positionClosingMargin := p.Margin.Mul(closingQuantity).Quo(p.Quantity)
   365  	payout = pnlNotional.Add(positionClosingMargin)
   366  
   367  	// for netting opposite direction
   368  	newPositionQuantity := p.Quantity.Sub(closingQuantity)
   369  	p.Margin = p.Margin.Mul(newPositionQuantity).Quo(p.Quantity)
   370  	p.Quantity = newPositionQuantity
   371  
   372  	isFlippingPosition := delta.ExecutionQuantity.GT(closingQuantity)
   373  
   374  	if isFlippingPosition {
   375  		remainingExecutionQuantity := delta.ExecutionQuantity.Sub(closingQuantity)
   376  		remainingExecutionMargin := delta.ExecutionMargin.Sub(closeExecutionMargin)
   377  
   378  		newPositionDelta := &PositionDelta{
   379  			IsLong:            !p.IsLong,
   380  			ExecutionQuantity: remainingExecutionQuantity,
   381  			ExecutionMargin:   remainingExecutionMargin,
   382  			ExecutionPrice:    delta.ExecutionPrice,
   383  		}
   384  
   385  		// recurse
   386  		_, _, collateralizationMargin, _ = p.ApplyPositionDelta(newPositionDelta, tradingFeeForReduceOnly)
   387  	}
   388  
   389  	return payout, closeExecutionMargin, collateralizationMargin, pnlNotional
   390  }