@drift-labs/sdk
Version:
SDK for Drift Protocol
333 lines (296 loc) • 8.08 kB
text/typescript
import { BN } from '@coral-xyz/anchor';
import {
AMM_RESERVE_PRECISION,
AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO,
AMM_TO_QUOTE_PRECISION_RATIO,
FUNDING_RATE_BUFFER_PRECISION,
PRICE_PRECISION,
ONE,
ZERO,
} from '../constants/numericConstants';
import { MMOraclePriceData, OraclePriceData } from '../oracles/types';
import {
PerpMarketAccount,
PositionDirection,
PerpPosition,
SpotMarketAccount,
} from '../types';
import {
calculateUpdatedAMM,
calculateUpdatedAMMSpreadReserves,
calculateAmmReservesAfterSwap,
getSwapDirection,
} from './amm';
import { calculateBaseAssetValueWithOracle } from './margin';
import { calculateNetUserPnlImbalance } from './market';
/**
* calculateBaseAssetValue
* = market value of closing entire position
* @param market
* @param userPosition
* @param oraclePriceData
* @returns Base Asset Value. : Precision QUOTE_PRECISION
*/
export function calculateBaseAssetValue(
market: PerpMarketAccount,
userPosition: PerpPosition,
mmOraclePriceData: MMOraclePriceData,
useSpread = true,
skipUpdate = false,
latestSlot?: BN
): BN {
if (userPosition.baseAssetAmount.eq(ZERO)) {
return ZERO;
}
const directionToClose = findDirectionToClose(userPosition);
let prepegAmm: Parameters<typeof calculateAmmReservesAfterSwap>[0];
if (!skipUpdate) {
if (market.amm.baseSpread > 0 && useSpread) {
const { baseAssetReserve, quoteAssetReserve, sqrtK, newPeg } =
calculateUpdatedAMMSpreadReserves(
market.amm,
directionToClose,
mmOraclePriceData,
undefined,
latestSlot
);
prepegAmm = {
baseAssetReserve,
quoteAssetReserve,
sqrtK: sqrtK,
pegMultiplier: newPeg,
};
} else {
prepegAmm = calculateUpdatedAMM(market.amm, mmOraclePriceData);
}
} else {
prepegAmm = market.amm;
}
const [newQuoteAssetReserve, _] = calculateAmmReservesAfterSwap(
prepegAmm,
'base',
userPosition.baseAssetAmount.abs(),
getSwapDirection('base', directionToClose)
);
switch (directionToClose) {
case PositionDirection.SHORT:
return prepegAmm.quoteAssetReserve
.sub(newQuoteAssetReserve)
.mul(prepegAmm.pegMultiplier)
.div(AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO);
case PositionDirection.LONG:
return newQuoteAssetReserve
.sub(prepegAmm.quoteAssetReserve)
.mul(prepegAmm.pegMultiplier)
.div(AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO)
.add(ONE);
}
}
/**
* calculatePositionPNL
* = BaseAssetAmount * (Avg Exit Price - Avg Entry Price)
* @param market
* @param PerpPosition
* @param withFunding (adds unrealized funding payment pnl to result)
* @param oraclePriceData
* @returns BaseAssetAmount : Precision QUOTE_PRECISION
*/
export function calculatePositionPNL(
market: PerpMarketAccount,
perpPosition: PerpPosition,
withFunding = false,
oraclePriceData: Pick<OraclePriceData, 'price'>
): BN {
if (perpPosition.baseAssetAmount.eq(ZERO)) {
return perpPosition.quoteAssetAmount;
}
const baseAssetValue = calculateBaseAssetValueWithOracle(
market,
perpPosition,
oraclePriceData
);
const baseAssetValueSign = perpPosition.baseAssetAmount.isNeg()
? new BN(-1)
: new BN(1);
let pnl = baseAssetValue
.mul(baseAssetValueSign)
.add(perpPosition.quoteAssetAmount);
if (withFunding) {
const fundingRatePnL = calculateUnsettledFundingPnl(market, perpPosition);
pnl = pnl.add(fundingRatePnL);
}
return pnl;
}
export function calculateClaimablePnl(
market: PerpMarketAccount,
spotMarket: SpotMarketAccount,
perpPosition: PerpPosition,
oraclePriceData: Pick<OraclePriceData, 'price'>
): BN {
const unrealizedPnl = calculatePositionPNL(
market,
perpPosition,
true,
oraclePriceData
);
let unsettledPnl = unrealizedPnl;
if (unrealizedPnl.gt(ZERO)) {
const excessPnlPool = BN.max(
ZERO,
calculateNetUserPnlImbalance(market, spotMarket, oraclePriceData).mul(
new BN(-1)
)
);
const maxPositivePnl = BN.max(
perpPosition.quoteAssetAmount.sub(perpPosition.quoteEntryAmount),
ZERO
).add(excessPnlPool);
unsettledPnl = BN.min(maxPositivePnl, unrealizedPnl);
}
return unsettledPnl;
}
/**
* Returns total fees and funding pnl for a position
*
* @param market
* @param PerpPosition
* @param includeUnsettled include unsettled funding in return value (default: true)
* @returns — // QUOTE_PRECISION
*/
export function calculateFeesAndFundingPnl(
market: PerpMarketAccount,
perpPosition: PerpPosition,
includeUnsettled = true
): BN {
const settledFundingAndFeesPnl = perpPosition.quoteBreakEvenAmount.sub(
perpPosition.quoteEntryAmount
);
if (!includeUnsettled) {
return settledFundingAndFeesPnl;
}
const unsettledFundingPnl = calculateUnsettledFundingPnl(
market,
perpPosition
);
return settledFundingAndFeesPnl.add(unsettledFundingPnl);
}
/**
* Returns unsettled funding pnl for the position
*
* To calculate all fees and funding pnl including settled, use calculateFeesAndFundingPnl
*
* @param market
* @param PerpPosition
* @returns // QUOTE_PRECISION
*/
export function calculateUnsettledFundingPnl(
market: PerpMarketAccount,
perpPosition: PerpPosition
): BN {
if (perpPosition.baseAssetAmount.eq(ZERO)) {
return ZERO;
}
let ammCumulativeFundingRate: BN;
if (perpPosition.baseAssetAmount.gt(ZERO)) {
ammCumulativeFundingRate = market.amm.cumulativeFundingRateLong;
} else {
ammCumulativeFundingRate = market.amm.cumulativeFundingRateShort;
}
const perPositionFundingRate = ammCumulativeFundingRate
.sub(perpPosition.lastCumulativeFundingRate)
.mul(perpPosition.baseAssetAmount)
.div(AMM_RESERVE_PRECISION)
.div(FUNDING_RATE_BUFFER_PRECISION)
.mul(new BN(-1));
return perPositionFundingRate;
}
/**
* @deprecated use calculateUnsettledFundingPnl or calculateFeesAndFundingPnl instead
*/
export function calculatePositionFundingPNL(
market: PerpMarketAccount,
perpPosition: PerpPosition
): BN {
return calculateUnsettledFundingPnl(market, perpPosition);
}
export function positionIsAvailable(position: PerpPosition): boolean {
return (
position.baseAssetAmount.eq(ZERO) &&
position.openOrders === 0 &&
position.quoteAssetAmount.eq(ZERO) &&
position.lpShares.eq(ZERO)
);
}
/**
*
* @param userPosition
* @returns Precision: PRICE_PRECISION (10^6)
*/
export function calculateBreakEvenPrice(userPosition: PerpPosition): BN {
if (userPosition.baseAssetAmount.eq(ZERO)) {
return ZERO;
}
return userPosition.quoteBreakEvenAmount
.mul(PRICE_PRECISION)
.mul(AMM_TO_QUOTE_PRECISION_RATIO)
.div(userPosition.baseAssetAmount)
.abs();
}
/**
*
* @param userPosition
* @returns Precision: PRICE_PRECISION (10^6)
*/
export function calculateEntryPrice(userPosition: PerpPosition): BN {
if (userPosition.baseAssetAmount.eq(ZERO)) {
return ZERO;
}
return userPosition.quoteEntryAmount
.mul(PRICE_PRECISION)
.mul(AMM_TO_QUOTE_PRECISION_RATIO)
.div(userPosition.baseAssetAmount)
.abs();
}
/**
*
* @param userPosition
* @returns Precision: PRICE_PRECISION (10^10)
*/
export function calculateCostBasis(
userPosition: PerpPosition,
includeSettledPnl = false
): BN {
if (userPosition.baseAssetAmount.eq(ZERO)) {
return ZERO;
}
return userPosition.quoteAssetAmount
.add(includeSettledPnl ? userPosition.settledPnl : ZERO)
.mul(PRICE_PRECISION)
.mul(AMM_TO_QUOTE_PRECISION_RATIO)
.div(userPosition.baseAssetAmount)
.abs();
}
export function findDirectionToClose(
userPosition: PerpPosition
): PositionDirection {
return userPosition.baseAssetAmount.gt(ZERO)
? PositionDirection.SHORT
: PositionDirection.LONG;
}
export function positionCurrentDirection(
userPosition: PerpPosition
): PositionDirection {
return userPosition.baseAssetAmount.gte(ZERO)
? PositionDirection.LONG
: PositionDirection.SHORT;
}
export function isEmptyPosition(userPosition: PerpPosition): boolean {
return userPosition.baseAssetAmount.eq(ZERO) && userPosition.openOrders === 0;
}
export function hasOpenOrders(position: PerpPosition): boolean {
return (
position.openOrders != 0 ||
!position.openBids.eq(ZERO) ||
!position.openAsks.eq(ZERO)
);
}