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 }