@drift-labs/sdk
Version:
SDK for Drift Protocol
250 lines (249 loc) • 13 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTriggerPrice = exports.calculatePerpMarketBaseLiquidatorFee = exports.calculateAvailablePerpLiquidity = exports.calculateNetUserPnlImbalance = exports.calculateNetUserPnl = exports.calculateMarketMaxAvailableInsurance = exports.calculateMarketAvailablePNL = exports.calculateUnrealizedAssetWeight = exports.calculateMarketMarginRatio = exports.calculateOracleSpread = exports.calculateOracleReserveSpread = exports.calculateNewMarketAfterTrade = exports.calculateAskPrice = exports.calculateBidPrice = exports.calculateReservePrice = void 0;
const anchor_1 = require("@coral-xyz/anchor");
const types_1 = require("../types");
const amm_1 = require("./amm");
const margin_1 = require("./margin");
const numericConstants_1 = require("../constants/numericConstants");
const spotBalance_1 = require("./spotBalance");
const assert_1 = require("../assert/assert");
/**
* Calculates market mark price
*
* @param market
* @return markPrice : Precision PRICE_PRECISION
*/
function calculateReservePrice(market, mmOraclePriceData) {
const newAmm = (0, amm_1.calculateUpdatedAMM)(market.amm, mmOraclePriceData);
return (0, amm_1.calculatePrice)(newAmm.baseAssetReserve, newAmm.quoteAssetReserve, newAmm.pegMultiplier);
}
exports.calculateReservePrice = calculateReservePrice;
/**
* Calculates market bid price
*
* @param market
* @return bidPrice : Precision PRICE_PRECISION
*/
function calculateBidPrice(market, mmOraclePriceData, latestSlot) {
const { baseAssetReserve, quoteAssetReserve, newPeg } = (0, amm_1.calculateUpdatedAMMSpreadReserves)(market.amm, types_1.PositionDirection.SHORT, mmOraclePriceData, undefined, latestSlot);
return (0, amm_1.calculatePrice)(baseAssetReserve, quoteAssetReserve, newPeg);
}
exports.calculateBidPrice = calculateBidPrice;
/**
* Calculates market ask price
*
* @param market
* @return askPrice : Precision PRICE_PRECISION
*/
function calculateAskPrice(market, mmOraclePriceData, latestSlot) {
const { baseAssetReserve, quoteAssetReserve, newPeg } = (0, amm_1.calculateUpdatedAMMSpreadReserves)(market.amm, types_1.PositionDirection.LONG, mmOraclePriceData, undefined, latestSlot);
return (0, amm_1.calculatePrice)(baseAssetReserve, quoteAssetReserve, newPeg);
}
exports.calculateAskPrice = calculateAskPrice;
function calculateNewMarketAfterTrade(baseAssetAmount, direction, market) {
const [newQuoteAssetReserve, newBaseAssetReserve] = (0, amm_1.calculateAmmReservesAfterSwap)(market.amm, 'base', baseAssetAmount.abs(), (0, amm_1.getSwapDirection)('base', direction));
const newAmm = Object.assign({}, market.amm);
const newMarket = Object.assign({}, market);
newMarket.amm = newAmm;
newMarket.amm.quoteAssetReserve = newQuoteAssetReserve;
newMarket.amm.baseAssetReserve = newBaseAssetReserve;
return newMarket;
}
exports.calculateNewMarketAfterTrade = calculateNewMarketAfterTrade;
function calculateOracleReserveSpread(market, mmOraclePriceData) {
const reservePrice = calculateReservePrice(market, mmOraclePriceData);
return calculateOracleSpread(reservePrice, mmOraclePriceData);
}
exports.calculateOracleReserveSpread = calculateOracleReserveSpread;
function calculateOracleSpread(price, oraclePriceData) {
return price.sub(oraclePriceData.price);
}
exports.calculateOracleSpread = calculateOracleSpread;
function calculateMarketMarginRatio(market, size, marginCategory, customMarginRatio = 0, userHighLeverageMode = false) {
let marginRationInitial;
let marginRatioMaintenance;
if (userHighLeverageMode &&
market.highLeverageMarginRatioInitial > 0 &&
market.highLeverageMarginRatioMaintenance) {
marginRationInitial = market.highLeverageMarginRatioInitial;
marginRatioMaintenance = market.highLeverageMarginRatioMaintenance;
}
else {
marginRationInitial = market.marginRatioInitial;
marginRatioMaintenance = market.marginRatioMaintenance;
}
let marginRatio;
switch (marginCategory) {
case 'Initial': {
// use lowest leverage between max allowed and optional user custom max
marginRatio = Math.max((0, margin_1.calculateSizePremiumLiabilityWeight)(size, new anchor_1.BN(market.imfFactor), new anchor_1.BN(marginRationInitial), numericConstants_1.MARGIN_PRECISION).toNumber(), customMarginRatio);
break;
}
case 'Maintenance': {
marginRatio = (0, margin_1.calculateSizePremiumLiabilityWeight)(size, new anchor_1.BN(market.imfFactor), new anchor_1.BN(marginRatioMaintenance), numericConstants_1.MARGIN_PRECISION).toNumber();
break;
}
}
return marginRatio;
}
exports.calculateMarketMarginRatio = calculateMarketMarginRatio;
function calculateUnrealizedAssetWeight(market, quoteSpotMarket, unrealizedPnl, marginCategory, oraclePriceData) {
let assetWeight;
switch (marginCategory) {
case 'Initial':
assetWeight = new anchor_1.BN(market.unrealizedPnlInitialAssetWeight);
if (market.unrealizedPnlMaxImbalance.gt(numericConstants_1.ZERO)) {
const netUnsettledPnl = calculateNetUserPnlImbalance(market, quoteSpotMarket, oraclePriceData);
if (netUnsettledPnl.gt(market.unrealizedPnlMaxImbalance)) {
assetWeight = assetWeight
.mul(market.unrealizedPnlMaxImbalance)
.div(netUnsettledPnl);
}
}
assetWeight = (0, margin_1.calculateSizeDiscountAssetWeight)(unrealizedPnl, new anchor_1.BN(market.unrealizedPnlImfFactor), assetWeight);
break;
case 'Maintenance':
assetWeight = new anchor_1.BN(market.unrealizedPnlMaintenanceAssetWeight);
break;
}
return assetWeight;
}
exports.calculateUnrealizedAssetWeight = calculateUnrealizedAssetWeight;
function calculateMarketAvailablePNL(perpMarket, spotMarket) {
return (0, spotBalance_1.getTokenAmount)(perpMarket.pnlPool.scaledBalance, spotMarket, types_1.SpotBalanceType.DEPOSIT);
}
exports.calculateMarketAvailablePNL = calculateMarketAvailablePNL;
function calculateMarketMaxAvailableInsurance(perpMarket, spotMarket) {
(0, assert_1.assert)(spotMarket.marketIndex == numericConstants_1.QUOTE_SPOT_MARKET_INDEX);
// todo: insuranceFundAllocation technically not guaranteed to be in Insurance Fund
const insuranceFundAllocation = perpMarket.insuranceClaim.quoteMaxInsurance.sub(perpMarket.insuranceClaim.quoteSettledInsurance);
const ammFeePool = (0, spotBalance_1.getTokenAmount)(perpMarket.amm.feePool.scaledBalance, spotMarket, types_1.SpotBalanceType.DEPOSIT);
return insuranceFundAllocation.add(ammFeePool);
}
exports.calculateMarketMaxAvailableInsurance = calculateMarketMaxAvailableInsurance;
function calculateNetUserPnl(perpMarket, oraclePriceData) {
const netUserPositionValue = perpMarket.amm.baseAssetAmountWithAmm
.add(perpMarket.amm.baseAssetAmountWithUnsettledLp)
.mul(oraclePriceData.price)
.div(numericConstants_1.BASE_PRECISION)
.div(numericConstants_1.PRICE_TO_QUOTE_PRECISION);
const netUserCostBasis = perpMarket.amm.quoteAssetAmount
.add(perpMarket.amm.quoteAssetAmountWithUnsettledLp)
.add(perpMarket.amm.netUnsettledFundingPnl);
const netUserPnl = netUserPositionValue.add(netUserCostBasis);
return netUserPnl;
}
exports.calculateNetUserPnl = calculateNetUserPnl;
function calculateNetUserPnlImbalance(perpMarket, spotMarket, oraclePriceData, applyFeePoolDiscount = true) {
const netUserPnl = calculateNetUserPnl(perpMarket, oraclePriceData);
const pnlPool = (0, spotBalance_1.getTokenAmount)(perpMarket.pnlPool.scaledBalance, spotMarket, types_1.SpotBalanceType.DEPOSIT);
let feePool = (0, spotBalance_1.getTokenAmount)(perpMarket.amm.feePool.scaledBalance, spotMarket, types_1.SpotBalanceType.DEPOSIT);
if (applyFeePoolDiscount) {
feePool = feePool.div(new anchor_1.BN(5));
}
const imbalance = netUserPnl.sub(pnlPool.add(feePool));
return imbalance;
}
exports.calculateNetUserPnlImbalance = calculateNetUserPnlImbalance;
function calculateAvailablePerpLiquidity(market, mmOraclePriceData, dlob, slot) {
let [bids, asks] = (0, amm_1.calculateMarketOpenBidAsk)(market.amm.baseAssetReserve, market.amm.minBaseAssetReserve, market.amm.maxBaseAssetReserve, market.amm.orderStepSize);
asks = asks.abs();
for (const bid of dlob.getRestingLimitBids(market.marketIndex, slot, types_1.MarketType.PERP, mmOraclePriceData)) {
bids = bids.add(bid.order.baseAssetAmount.sub(bid.order.baseAssetAmountFilled));
}
for (const ask of dlob.getRestingLimitAsks(market.marketIndex, slot, types_1.MarketType.PERP, mmOraclePriceData)) {
asks = asks.add(ask.order.baseAssetAmount.sub(ask.order.baseAssetAmountFilled));
}
return {
bids: bids,
asks: asks,
};
}
exports.calculateAvailablePerpLiquidity = calculateAvailablePerpLiquidity;
function calculatePerpMarketBaseLiquidatorFee(market, userHighLeverageMode) {
if (userHighLeverageMode && market.highLeverageMarginRatioMaintenance > 0) {
const marginRatio = market.highLeverageMarginRatioMaintenance * 100;
// min(liquidator_fee, .8 * high_leverage_margin_ratio_maintenance)
return Math.min(market.liquidatorFee, marginRatio - Math.floor(marginRatio / 5));
}
else {
return market.liquidatorFee;
}
}
exports.calculatePerpMarketBaseLiquidatorFee = calculatePerpMarketBaseLiquidatorFee;
/**
* Calculates trigger price for a perp market based on oracle price and current time
* Implements the same logic as the Rust get_trigger_price function
*
* @param market - The perp market account
* @param oraclePrice - Current oracle price (precision: PRICE_PRECISION)
* @param now - Current timestamp in seconds
* @returns trigger price (precision: PRICE_PRECISION)
*/
function getTriggerPrice(market, oraclePrice, now, useMedianPrice) {
if (!useMedianPrice) {
return oraclePrice.abs();
}
const lastFillPrice = market.lastFillPrice;
// Calculate 5-minute basis
const markPrice5minTwap = market.amm.lastMarkPriceTwap5Min;
const lastOraclePriceTwap5min = market.amm.historicalOracleData.lastOraclePriceTwap5Min;
const basis5min = markPrice5minTwap.sub(lastOraclePriceTwap5min);
const oraclePlusBasis5min = oraclePrice.add(basis5min);
// Calculate funding basis
const lastFundingBasis = getLastFundingBasis(market, oraclePrice, now);
const oraclePlusFundingBasis = oraclePrice.add(lastFundingBasis);
const prices = [
lastFillPrice.gt(numericConstants_1.ZERO) ? lastFillPrice : oraclePrice,
oraclePlusFundingBasis,
oraclePlusBasis5min,
].sort((a, b) => a.cmp(b));
const medianPrice = prices[1];
return clampTriggerPrice(market, oraclePrice.abs(), medianPrice);
}
exports.getTriggerPrice = getTriggerPrice;
/**
* Calculates the last funding basis for trigger price calculation
* Implements the same logic as the Rust get_last_funding_basis function
*/
function getLastFundingBasis(market, oraclePrice, now) {
if (market.amm.lastFundingOracleTwap.gt(numericConstants_1.ZERO)) {
const lastFundingRate = market.amm.lastFundingRate
.mul(numericConstants_1.PRICE_PRECISION)
.div(market.amm.lastFundingOracleTwap)
.muln(24);
const lastFundingRatePreAdj = lastFundingRate.sub(numericConstants_1.FUNDING_RATE_PRECISION.div(new anchor_1.BN(5000)) // FUNDING_RATE_OFFSET_PERCENTAGE
);
const timeLeftUntilFundingUpdate = anchor_1.BN.min(anchor_1.BN.max(now.sub(market.amm.lastFundingRateTs), numericConstants_1.ZERO), market.amm.fundingPeriod);
const lastFundingBasis = oraclePrice
.mul(lastFundingRatePreAdj)
.div(numericConstants_1.PERCENTAGE_PRECISION)
.mul(market.amm.fundingPeriod.sub(timeLeftUntilFundingUpdate))
.div(market.amm.fundingPeriod)
.div(new anchor_1.BN(1000)); // FUNDING_RATE_BUFFER
return lastFundingBasis;
}
else {
return numericConstants_1.ZERO;
}
}
/**
* Clamps trigger price based on contract tier
* Implements the same logic as the Rust clamp_trigger_price function
*/
function clampTriggerPrice(market, oraclePrice, medianPrice) {
let maxBpsDiff;
const tier = market.contractTier;
if ((0, types_1.isVariant)(tier, 'a') || (0, types_1.isVariant)(tier, 'b')) {
maxBpsDiff = new anchor_1.BN(500); // 20 BPS
}
else if ((0, types_1.isVariant)(tier, 'c')) {
maxBpsDiff = new anchor_1.BN(100); // 100 BPS
}
else {
maxBpsDiff = new anchor_1.BN(40); // 250 BPS
}
const maxOracleDiff = oraclePrice.div(maxBpsDiff);
return anchor_1.BN.min(anchor_1.BN.max(medianPrice, oraclePrice.sub(maxOracleDiff)), oraclePrice.add(maxOracleDiff));
}
;