Overview
Drift Protocol calculates Profit and Loss (PnL) for perpetual positions based on the difference between entry and exit prices, including funding payments and fees.Position PnL Formula
The basic PnL formula:PnL = baseAssetAmount × (exitPrice - entryPrice) + quoteAssetAmount
baseAssetAmount- Position size (positive for long, negative for short)exitPrice- Current market price or oracle priceentryPrice- Average entry pricequoteAssetAmount- Accumulated quote changes from trades
Calculating Position PnL
/**
* Calculate position PnL including optional funding payments
*
* @param market - PerpMarketAccount
* @param perpPosition - User's perp position
* @param withFunding - Include unsettled funding payment in PnL
* @param oraclePriceData - Oracle price data
* @returns PnL in QUOTE_PRECISION (10^6)
*/
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;
}
Example
import {
calculatePositionPNL,
QUOTE_PRECISION,
convertToNumber
} from '@drift-labs/sdk';
const user = /* User instance */;
const position = user.getPerpPosition(0); // SOL-PERP
const market = driftClient.getPerpMarketAccount(0);
const oracleData = driftClient.getOracleDataForPerpMarket(0);
// Calculate PnL without funding
const pnlBN = calculatePositionPNL(market, position, false, oracleData);
const pnl = convertToNumber(pnlBN, QUOTE_PRECISION);
console.log(`PnL: $${pnl}`);
// Calculate PnL with funding
const pnlWithFundingBN = calculatePositionPNL(market, position, true, oracleData);
const pnlWithFunding = convertToNumber(pnlWithFundingBN, QUOTE_PRECISION);
console.log(`PnL with funding: $${pnlWithFunding}`);
Base Asset Value
Calculate the value of the base asset amount at current prices: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);
}
}
Base asset value represents the market value of closing the entire position at current AMM prices.
Oracle-Based Asset Value
Simpler calculation using oracle price directly:export function calculateBaseAssetValueWithOracle(
market: PerpMarketAccount,
perpPosition: PerpPosition,
oraclePriceData: Pick<OraclePriceData, 'price'>,
includeOpenOrders = false
): BN {
let price = oraclePriceData.price;
if (isVariant(market.status, 'settlement')) {
price = market.expiryPrice;
}
const baseAssetAmount = includeOpenOrders
? calculateWorstCaseBaseAssetAmount(
perpPosition,
market,
oraclePriceData.price
)
: perpPosition.baseAssetAmount;
return baseAssetAmount.abs().mul(price).div(AMM_RESERVE_PRECISION);
}
Funding PnL
Funding payments are settled periodically between longs and shorts:/**
* Calculate unsettled funding payment PnL
*
* @param market - PerpMarketAccount
* @param perpPosition - User's perp position
* @returns Unsettled funding PnL in QUOTE_PRECISION (10^6)
*/
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;
}
Formula
fundingPnL = -(cumulativeFundingRate - lastCumulativeFundingRate) × baseAssetAmount / (AMM_RESERVE_PRECISION × FUNDING_RATE_BUFFER_PRECISION)
Example
import {
calculateUnsettledFundingPnl,
QUOTE_PRECISION,
convertToNumber
} from '@drift-labs/sdk';
const position = user.getPerpPosition(0);
const market = driftClient.getPerpMarketAccount(0);
const fundingPnlBN = calculateUnsettledFundingPnl(market, position);
const fundingPnl = convertToNumber(fundingPnlBN, QUOTE_PRECISION);
if (fundingPnl > 0) {
console.log(`Receiving $${fundingPnl} in funding`);
} else {
console.log(`Paying $${Math.abs(fundingPnl)} in funding`);
}
Fees and Funding Combined
Get total fees and funding for a position:/**
* Calculate total fees and funding PnL
*
* @param market - PerpMarketAccount
* @param perpPosition - User's perp position
* @param includeUnsettled - Include unsettled funding (default: true)
* @returns Total fees and funding in QUOTE_PRECISION (10^6)
*/
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);
}
Entry Price
Calculate the average entry price of a position:/**
* Calculate average entry price
*
* @param userPosition - User's perp position
* @returns Entry price in 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();
}
Formula
entryPrice = |quoteEntryAmount × PRICE_PRECISION × AMM_TO_QUOTE_PRECISION_RATIO / baseAssetAmount|
Break-Even Price
Calculate the break-even price (including fees and funding):/**
* Calculate break-even price (entry price + fees + funding)
*
* @param userPosition - User's perp position
* @returns Break-even price in 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();
}
Example
import {
calculateEntryPrice,
calculateBreakEvenPrice,
PRICE_PRECISION,
convertToNumber
} from '@drift-labs/sdk';
const position = user.getPerpPosition(0);
const entryPrice = convertToNumber(
calculateEntryPrice(position),
PRICE_PRECISION
);
const breakEvenPrice = convertToNumber(
calculateBreakEvenPrice(position),
PRICE_PRECISION
);
const feesAndFunding = breakEvenPrice - entryPrice;
console.log(`Entry: $${entryPrice}`);
console.log(`Break-even: $${breakEvenPrice}`);
console.log(`Fees + Funding: $${feesAndFunding}`);
Cost Basis
Calculate the current cost basis of a position:/**
* Calculate cost basis (current quote value per unit of base)
*
* @param userPosition - User's perp position
* @param includeSettledPnl - Include settled PnL in cost basis
* @returns Cost basis in PRICE_PRECISION (10^6)
*/
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();
}
Claimable PnL
Not all unrealized PnL can be immediately settled: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, oracleData).mul(
new BN(-1)
)
);
const maxPositivePnl = BN.max(
perpPosition.quoteAssetAmount.sub(perpPosition.quoteEntryAmount),
ZERO
).add(excessPnlPool);
unsettledPnl = BN.min(maxPositivePnl, unrealizedPnl);
}
return unsettledPnl;
}
Claimable PnL may be less than unrealized PnL due to the PnL pool mechanism that prevents bank runs.
Practical Examples
Complete Position Summary
import {
calculatePositionPNL,
calculateEntryPrice,
calculateBreakEvenPrice,
calculateFeesAndFundingPnl,
PRICE_PRECISION,
QUOTE_PRECISION,
BASE_PRECISION,
convertToNumber
} from '@drift-labs/sdk';
const marketIndex = 0; // SOL-PERP
const position = user.getPerpPosition(marketIndex);
const market = driftClient.getPerpMarketAccount(marketIndex);
const oracleData = driftClient.getOracleDataForPerpMarket(marketIndex);
// Position details
const size = convertToNumber(position.baseAssetAmount, BASE_PRECISION);
const currentPrice = convertToNumber(oracleData.price, PRICE_PRECISION);
const entryPrice = convertToNumber(calculateEntryPrice(position), PRICE_PRECISION);
const breakEven = convertToNumber(calculateBreakEvenPrice(position), PRICE_PRECISION);
// PnL breakdown
const totalPnl = convertToNumber(
calculatePositionPNL(market, position, true, oracleData),
QUOTE_PRECISION
);
const feesAndFunding = convertToNumber(
calculateFeesAndFundingPnl(market, position),
QUOTE_PRECISION
);
const tradePnl = totalPnl - feesAndFunding;
console.log('Position Summary:');
console.log(`Size: ${size} SOL`);
console.log(`Entry: $${entryPrice}`);
console.log(`Current: $${currentPrice}`);
console.log(`Break-even: $${breakEven}`);
console.log('\nPnL Breakdown:');
console.log(`Trade PnL: $${tradePnl}`);
console.log(`Fees + Funding: $${feesAndFunding}`);
console.log(`Total PnL: $${totalPnl}`);
Calculate Return on Investment (ROI)
import {
calculatePositionPNL,
calculateMarginUSDCRequiredForTrade,
QUOTE_PRECISION,
convertToNumber
} from '@drift-labs/sdk';
const position = user.getPerpPosition(0);
const market = driftClient.getPerpMarketAccount(0);
const oracleData = driftClient.getOracleDataForPerpMarket(0);
const pnl = convertToNumber(
calculatePositionPNL(market, position, true, oracleData),
QUOTE_PRECISION
);
const marginUsed = convertToNumber(
calculateMarginUSDCRequiredForTrade(
driftClient,
0,
position.baseAssetAmount.abs()
),
QUOTE_PRECISION
);
const roi = (pnl / marginUsed) * 100;
console.log(`ROI: ${roi.toFixed(2)}%`);