@drift-labs/sdk-browser
Version:
SDK for Drift Protocol
1,901 lines (1,688 loc) • 105 kB
text/typescript
import { PublicKey } from '@solana/web3.js';
import { EventEmitter } from 'events';
import StrictEventEmitter from 'strict-event-emitter-types';
import { DriftClient } from './driftClient';
import {
HealthComponent,
HealthComponents,
isVariant,
MarginCategory,
Order,
PerpMarketAccount,
PerpPosition,
SpotPosition,
UserAccount,
UserStatus,
UserStatsAccount,
} from './types';
import {
calculateEntryPrice,
calculateUnsettledFundingPnl,
positionIsAvailable,
} from './math/position';
import {
AMM_RESERVE_PRECISION,
AMM_TO_QUOTE_PRECISION_RATIO,
BASE_PRECISION,
BN_MAX,
DUST_POSITION_SIZE,
FIVE_MINUTE,
MARGIN_PRECISION,
OPEN_ORDER_MARGIN_REQUIREMENT,
PRICE_PRECISION,
QUOTE_PRECISION,
QUOTE_PRECISION_EXP,
QUOTE_SPOT_MARKET_INDEX,
SPOT_MARKET_WEIGHT_PRECISION,
TEN_THOUSAND,
TWO,
ZERO,
FUEL_START_TS,
} from './constants/numericConstants';
import {
DataAndSlot,
UserAccountEvents,
UserAccountSubscriber,
} from './accounts/types';
import { BigNum } from './factory/bigNum';
import { BN } from '@coral-xyz/anchor';
import { calculateBaseAssetValue, calculatePositionPNL } from './math/position';
import {
calculateMarketMarginRatio,
calculateReservePrice,
calculateUnrealizedAssetWeight,
} from './math/market';
import {
calculatePerpLiabilityValue,
calculateWorstCasePerpLiabilityValue,
} from './math/margin';
import { calculateSpotMarketMarginRatio } from './math/spotMarket';
import { divCeil, sigNum } from './math/utils';
import {
getBalance,
getSignedTokenAmount,
getStrictTokenValue,
getTokenValue,
} from './math/spotBalance';
import { getUser30dRollingVolumeEstimate } from './math/trade';
import {
MarketType,
PositionDirection,
SpotBalanceType,
SpotMarketAccount,
} from './types';
import { standardizeBaseAssetAmount } from './math/orders';
import { UserStats } from './userStats';
import { WebSocketProgramUserAccountSubscriber } from './accounts/websocketProgramUserAccountSubscriber';
import {
calculateAssetWeight,
calculateLiabilityWeight,
calculateWithdrawLimit,
getSpotAssetValue,
getSpotLiabilityValue,
getTokenAmount,
} from './math/spotBalance';
import {
calculateBaseAssetValueWithOracle,
calculateCollateralDepositRequiredForTrade,
calculateMarginUSDCRequiredForTrade,
calculateWorstCaseBaseAssetAmount,
} from './math/margin';
import { MMOraclePriceData, OraclePriceData } from './oracles/types';
import { UserConfig } from './userConfig';
import { PollingUserAccountSubscriber } from './accounts/pollingUserAccountSubscriber';
import { WebSocketUserAccountSubscriber } from './accounts/webSocketUserAccountSubscriber';
import {
calculateWeightedTokenValue,
getWorstCaseTokenAmounts,
isSpotPositionAvailable,
} from './math/spotPosition';
import {
calculateLiveOracleTwap,
getMultipleBetweenOracleSources,
} from './math/oracles';
import { getPerpMarketTierNumber, getSpotMarketTierNumber } from './math/tiers';
import { StrictOraclePrice } from './oracles/strictOraclePrice';
import { calculateSpotFuelBonus, calculatePerpFuelBonus } from './math/fuel';
import { grpcUserAccountSubscriber } from './accounts/grpcUserAccountSubscriber';
export class User {
driftClient: DriftClient;
userAccountPublicKey: PublicKey;
accountSubscriber: UserAccountSubscriber;
_isSubscribed = false;
eventEmitter: StrictEventEmitter<EventEmitter, UserAccountEvents>;
public get isSubscribed() {
return this._isSubscribed && this.accountSubscriber.isSubscribed;
}
public set isSubscribed(val: boolean) {
this._isSubscribed = val;
}
public constructor(config: UserConfig) {
this.driftClient = config.driftClient;
this.userAccountPublicKey = config.userAccountPublicKey;
if (config.accountSubscription?.type === 'polling') {
this.accountSubscriber = new PollingUserAccountSubscriber(
config.driftClient.connection,
config.userAccountPublicKey,
config.accountSubscription.accountLoader,
this.driftClient.program.account.user.coder.accounts.decodeUnchecked.bind(
this.driftClient.program.account.user.coder.accounts
)
);
} else if (config.accountSubscription?.type === 'custom') {
this.accountSubscriber = config.accountSubscription.userAccountSubscriber;
} else if (config.accountSubscription?.type === 'grpc') {
this.accountSubscriber = new grpcUserAccountSubscriber(
config.accountSubscription.grpcConfigs,
config.driftClient.program,
config.userAccountPublicKey,
{
resubTimeoutMs: config.accountSubscription?.resubTimeoutMs,
logResubMessages: config.accountSubscription?.logResubMessages,
}
);
} else {
if (
config.accountSubscription?.type === 'websocket' &&
config.accountSubscription?.programUserAccountSubscriber
) {
this.accountSubscriber = new WebSocketProgramUserAccountSubscriber(
config.driftClient.program,
config.userAccountPublicKey,
config.accountSubscription.programUserAccountSubscriber
);
} else {
this.accountSubscriber = new WebSocketUserAccountSubscriber(
config.driftClient.program,
config.userAccountPublicKey,
{
resubTimeoutMs: config.accountSubscription?.resubTimeoutMs,
logResubMessages: config.accountSubscription?.logResubMessages,
},
config.accountSubscription?.commitment
);
}
}
this.eventEmitter = this.accountSubscriber.eventEmitter;
}
/**
* Subscribe to User state accounts
* @returns SusbcriptionSuccess result
*/
public async subscribe(userAccount?: UserAccount): Promise<boolean> {
this.isSubscribed = await this.accountSubscriber.subscribe(userAccount);
return this.isSubscribed;
}
/**
* Forces the accountSubscriber to fetch account updates from rpc
*/
public async fetchAccounts(): Promise<void> {
await this.accountSubscriber.fetch();
}
public async unsubscribe(): Promise<void> {
await this.accountSubscriber.unsubscribe();
this.isSubscribed = false;
}
public getUserAccount(): UserAccount {
return this.accountSubscriber.getUserAccountAndSlot().data;
}
public async forceGetUserAccount(): Promise<UserAccount> {
await this.fetchAccounts();
return this.accountSubscriber.getUserAccountAndSlot().data;
}
public getUserAccountAndSlot(): DataAndSlot<UserAccount> | undefined {
return this.accountSubscriber.getUserAccountAndSlot();
}
public getPerpPositionForUserAccount(
userAccount: UserAccount,
marketIndex: number
): PerpPosition | undefined {
return this.getActivePerpPositionsForUserAccount(userAccount).find(
(position) => position.marketIndex === marketIndex
);
}
/**
* Gets the user's current position for a given perp market. If the user has no position returns undefined
* @param marketIndex
* @returns userPerpPosition
*/
public getPerpPosition(marketIndex: number): PerpPosition | undefined {
const userAccount = this.getUserAccount();
return this.getPerpPositionForUserAccount(userAccount, marketIndex);
}
public getPerpPositionOrEmpty(marketIndex: number): PerpPosition {
const userAccount = this.getUserAccount();
return (
this.getPerpPositionForUserAccount(userAccount, marketIndex) ??
this.getEmptyPosition(marketIndex)
);
}
public getPerpPositionAndSlot(
marketIndex: number
): DataAndSlot<PerpPosition | undefined> {
const userAccount = this.getUserAccountAndSlot();
const perpPosition = this.getPerpPositionForUserAccount(
userAccount.data,
marketIndex
);
return {
data: perpPosition,
slot: userAccount.slot,
};
}
public getSpotPositionForUserAccount(
userAccount: UserAccount,
marketIndex: number
): SpotPosition | undefined {
return userAccount.spotPositions.find(
(position) => position.marketIndex === marketIndex
);
}
/**
* Gets the user's current position for a given spot market. If the user has no position returns undefined
* @param marketIndex
* @returns userSpotPosition
*/
public getSpotPosition(marketIndex: number): SpotPosition | undefined {
const userAccount = this.getUserAccount();
return this.getSpotPositionForUserAccount(userAccount, marketIndex);
}
public getSpotPositionAndSlot(
marketIndex: number
): DataAndSlot<SpotPosition | undefined> {
const userAccount = this.getUserAccountAndSlot();
const spotPosition = this.getSpotPositionForUserAccount(
userAccount.data,
marketIndex
);
return {
data: spotPosition,
slot: userAccount.slot,
};
}
getEmptySpotPosition(marketIndex: number): SpotPosition {
return {
marketIndex,
scaledBalance: ZERO,
balanceType: SpotBalanceType.DEPOSIT,
cumulativeDeposits: ZERO,
openAsks: ZERO,
openBids: ZERO,
openOrders: 0,
};
}
/**
* Returns the token amount for a given market. The spot market precision is based on the token mint decimals.
* Positive if it is a deposit, negative if it is a borrow.
*
* @param marketIndex
*/
public getTokenAmount(marketIndex: number): BN {
const spotPosition = this.getSpotPosition(marketIndex);
if (spotPosition === undefined) {
return ZERO;
}
const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex);
return getSignedTokenAmount(
getTokenAmount(
spotPosition.scaledBalance,
spotMarket,
spotPosition.balanceType
),
spotPosition.balanceType
);
}
public getEmptyPosition(marketIndex: number): PerpPosition {
return {
baseAssetAmount: ZERO,
remainderBaseAssetAmount: 0,
lastCumulativeFundingRate: ZERO,
marketIndex,
quoteAssetAmount: ZERO,
quoteEntryAmount: ZERO,
quoteBreakEvenAmount: ZERO,
openOrders: 0,
openBids: ZERO,
openAsks: ZERO,
settledPnl: ZERO,
lpShares: ZERO,
lastBaseAssetAmountPerLp: ZERO,
lastQuoteAssetAmountPerLp: ZERO,
perLpBase: 0,
maxMarginRatio: 0,
};
}
public getClonedPosition(position: PerpPosition): PerpPosition {
const clonedPosition = Object.assign({}, position);
return clonedPosition;
}
public getOrderForUserAccount(
userAccount: UserAccount,
orderId: number
): Order | undefined {
return userAccount.orders.find((order) => order.orderId === orderId);
}
/**
* @param orderId
* @returns Order
*/
public getOrder(orderId: number): Order | undefined {
const userAccount = this.getUserAccount();
return this.getOrderForUserAccount(userAccount, orderId);
}
public getOrderAndSlot(orderId: number): DataAndSlot<Order | undefined> {
const userAccount = this.getUserAccountAndSlot();
const order = this.getOrderForUserAccount(userAccount.data, orderId);
return {
data: order,
slot: userAccount.slot,
};
}
public getOrderByUserIdForUserAccount(
userAccount: UserAccount,
userOrderId: number
): Order | undefined {
return userAccount.orders.find(
(order) => order.userOrderId === userOrderId
);
}
/**
* @param userOrderId
* @returns Order
*/
public getOrderByUserOrderId(userOrderId: number): Order | undefined {
const userAccount = this.getUserAccount();
return this.getOrderByUserIdForUserAccount(userAccount, userOrderId);
}
public getOrderByUserOrderIdAndSlot(
userOrderId: number
): DataAndSlot<Order | undefined> {
const userAccount = this.getUserAccountAndSlot();
const order = this.getOrderByUserIdForUserAccount(
userAccount.data,
userOrderId
);
return {
data: order,
slot: userAccount.slot,
};
}
public getOpenOrdersForUserAccount(userAccount?: UserAccount): Order[] {
return userAccount?.orders.filter((order) =>
isVariant(order.status, 'open')
);
}
public getOpenOrders(): Order[] {
const userAccount = this.getUserAccount();
return this.getOpenOrdersForUserAccount(userAccount);
}
public getOpenOrdersAndSlot(): DataAndSlot<Order[]> {
const userAccount = this.getUserAccountAndSlot();
const openOrders = this.getOpenOrdersForUserAccount(userAccount.data);
return {
data: openOrders,
slot: userAccount.slot,
};
}
public getUserAccountPublicKey(): PublicKey {
return this.userAccountPublicKey;
}
public async exists(): Promise<boolean> {
const userAccountRPCResponse =
await this.driftClient.connection.getParsedAccountInfo(
this.userAccountPublicKey
);
return userAccountRPCResponse.value !== null;
}
/**
* calculates the total open bids/asks in a perp market (including lps)
* @returns : open bids
* @returns : open asks
*/
public getPerpBidAsks(marketIndex: number): [BN, BN] {
const position = this.getPerpPosition(marketIndex);
const totalOpenBids = position.openBids;
const totalOpenAsks = position.openAsks;
return [totalOpenBids, totalOpenAsks];
}
/**
* calculates Buying Power = free collateral / initial margin ratio
* @returns : Precision QUOTE_PRECISION
*/
public getPerpBuyingPower(
marketIndex: number,
collateralBuffer = ZERO,
enterHighLeverageMode = undefined
): BN {
const perpPosition = this.getPerpPositionOrEmpty(marketIndex);
const perpMarket = this.driftClient.getPerpMarketAccount(marketIndex);
const oraclePriceData = this.getOracleDataForPerpMarket(marketIndex);
const worstCaseBaseAssetAmount = perpPosition
? calculateWorstCaseBaseAssetAmount(
perpPosition,
perpMarket,
oraclePriceData.price
)
: ZERO;
const freeCollateral = this.getFreeCollateral(
'Initial',
enterHighLeverageMode
).sub(collateralBuffer);
return this.getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount(
marketIndex,
freeCollateral,
worstCaseBaseAssetAmount,
enterHighLeverageMode,
perpPosition
);
}
getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount(
marketIndex: number,
freeCollateral: BN,
baseAssetAmount: BN,
enterHighLeverageMode = undefined,
perpPosition?: PerpPosition
): BN {
const userCustomMargin = Math.max(
perpPosition?.maxMarginRatio ?? 0,
this.getUserAccount().maxMarginRatio
);
const marginRatio = calculateMarketMarginRatio(
this.driftClient.getPerpMarketAccount(marketIndex),
baseAssetAmount,
'Initial',
userCustomMargin,
enterHighLeverageMode || this.isHighLeverageMode('Initial')
);
return freeCollateral.mul(MARGIN_PRECISION).div(new BN(marginRatio));
}
/**
* calculates Free Collateral = Total collateral - margin requirement
* @returns : Precision QUOTE_PRECISION
*/
public getFreeCollateral(
marginCategory: MarginCategory = 'Initial',
enterHighLeverageMode = undefined
): BN {
const totalCollateral = this.getTotalCollateral(marginCategory, true);
const marginRequirement =
marginCategory === 'Initial'
? this.getInitialMarginRequirement(enterHighLeverageMode)
: this.getMaintenanceMarginRequirement();
const freeCollateral = totalCollateral.sub(marginRequirement);
return freeCollateral.gte(ZERO) ? freeCollateral : ZERO;
}
/**
* @returns The margin requirement of a certain type (Initial or Maintenance) in USDC. : QUOTE_PRECISION
*/
public getMarginRequirement(
marginCategory: MarginCategory,
liquidationBuffer?: BN,
strict = false,
includeOpenOrders = true,
enteringHighLeverage = undefined
): BN {
return this.getTotalPerpPositionLiability(
marginCategory,
liquidationBuffer,
includeOpenOrders,
strict,
enteringHighLeverage
).add(
this.getSpotMarketLiabilityValue(
undefined,
marginCategory,
liquidationBuffer,
includeOpenOrders,
strict
)
);
}
/**
* @returns The initial margin requirement in USDC. : QUOTE_PRECISION
*/
public getInitialMarginRequirement(enterHighLeverageMode = undefined): BN {
return this.getMarginRequirement(
'Initial',
undefined,
true,
undefined,
enterHighLeverageMode
);
}
/**
* @returns The maintenance margin requirement in USDC. : QUOTE_PRECISION
*/
public getMaintenanceMarginRequirement(liquidationBuffer?: BN): BN {
return this.getMarginRequirement('Maintenance', liquidationBuffer);
}
public getActivePerpPositionsForUserAccount(
userAccount: UserAccount
): PerpPosition[] {
return userAccount.perpPositions.filter(
(pos) =>
!pos.baseAssetAmount.eq(ZERO) ||
!pos.quoteAssetAmount.eq(ZERO) ||
!(pos.openOrders == 0)
);
}
public getActivePerpPositions(): PerpPosition[] {
const userAccount = this.getUserAccount();
return this.getActivePerpPositionsForUserAccount(userAccount);
}
public getActivePerpPositionsAndSlot(): DataAndSlot<PerpPosition[]> {
const userAccount = this.getUserAccountAndSlot();
const positions = this.getActivePerpPositionsForUserAccount(
userAccount.data
);
return {
data: positions,
slot: userAccount.slot,
};
}
public getActiveSpotPositionsForUserAccount(
userAccount: UserAccount
): SpotPosition[] {
return userAccount.spotPositions.filter(
(pos) => !isSpotPositionAvailable(pos)
);
}
public getActiveSpotPositions(): SpotPosition[] {
const userAccount = this.getUserAccount();
return this.getActiveSpotPositionsForUserAccount(userAccount);
}
public getActiveSpotPositionsAndSlot(): DataAndSlot<SpotPosition[]> {
const userAccount = this.getUserAccountAndSlot();
const positions = this.getActiveSpotPositionsForUserAccount(
userAccount.data
);
return {
data: positions,
slot: userAccount.slot,
};
}
/**
* calculates unrealized position price pnl
* @returns : Precision QUOTE_PRECISION
*/
public getUnrealizedPNL(
withFunding?: boolean,
marketIndex?: number,
withWeightMarginCategory?: MarginCategory,
strict = false,
liquidationBuffer?: BN
): BN {
return this.getActivePerpPositions()
.filter((pos) =>
marketIndex !== undefined ? pos.marketIndex === marketIndex : true
)
.reduce((unrealizedPnl, perpPosition) => {
const market = this.driftClient.getPerpMarketAccount(
perpPosition.marketIndex
);
const oraclePriceData = this.getMMOracleDataForPerpMarket(
market.marketIndex
);
const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
market.quoteSpotMarketIndex
);
const quoteOraclePriceData = this.getOracleDataForSpotMarket(
market.quoteSpotMarketIndex
);
let positionUnrealizedPnl = calculatePositionPNL(
market,
perpPosition,
withFunding,
oraclePriceData
);
let quotePrice;
if (strict && positionUnrealizedPnl.gt(ZERO)) {
quotePrice = BN.min(
quoteOraclePriceData.price,
quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
);
} else if (strict && positionUnrealizedPnl.lt(ZERO)) {
quotePrice = BN.max(
quoteOraclePriceData.price,
quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
);
} else {
quotePrice = quoteOraclePriceData.price;
}
positionUnrealizedPnl = positionUnrealizedPnl
.mul(quotePrice)
.div(PRICE_PRECISION);
if (withWeightMarginCategory !== undefined) {
if (positionUnrealizedPnl.gt(ZERO)) {
positionUnrealizedPnl = positionUnrealizedPnl
.mul(
calculateUnrealizedAssetWeight(
market,
quoteSpotMarket,
positionUnrealizedPnl,
withWeightMarginCategory,
oraclePriceData
)
)
.div(new BN(SPOT_MARKET_WEIGHT_PRECISION));
}
if (liquidationBuffer && positionUnrealizedPnl.lt(ZERO)) {
positionUnrealizedPnl = positionUnrealizedPnl.add(
positionUnrealizedPnl.mul(liquidationBuffer).div(MARGIN_PRECISION)
);
}
}
return unrealizedPnl.add(positionUnrealizedPnl);
}, ZERO);
}
/**
* calculates unrealized funding payment pnl
* @returns : Precision QUOTE_PRECISION
*/
public getUnrealizedFundingPNL(marketIndex?: number): BN {
return this.getUserAccount()
.perpPositions.filter((pos) =>
marketIndex !== undefined ? pos.marketIndex === marketIndex : true
)
.reduce((pnl, perpPosition) => {
const market = this.driftClient.getPerpMarketAccount(
perpPosition.marketIndex
);
return pnl.add(calculateUnsettledFundingPnl(market, perpPosition));
}, ZERO);
}
public getFuelBonus(
now: BN,
includeSettled = true,
includeUnsettled = true,
givenUserStats?: UserStats
): {
depositFuel: BN;
borrowFuel: BN;
positionFuel: BN;
takerFuel: BN;
makerFuel: BN;
insuranceFuel: BN;
} {
const userAccount: UserAccount = this.getUserAccount();
const result = {
insuranceFuel: ZERO,
takerFuel: ZERO,
makerFuel: ZERO,
depositFuel: ZERO,
borrowFuel: ZERO,
positionFuel: ZERO,
};
const userStats = givenUserStats ?? this.driftClient.getUserStats();
const userStatsAccount: UserStatsAccount = userStats.getAccount();
if (includeSettled) {
result.takerFuel = result.takerFuel.add(
new BN(userStatsAccount.fuelTaker)
);
result.makerFuel = result.makerFuel.add(
new BN(userStatsAccount.fuelMaker)
);
result.depositFuel = result.depositFuel.add(
new BN(userStatsAccount.fuelDeposits)
);
result.borrowFuel = result.borrowFuel.add(
new BN(userStatsAccount.fuelBorrows)
);
result.positionFuel = result.positionFuel.add(
new BN(userStatsAccount.fuelPositions)
);
}
if (includeUnsettled) {
const fuelBonusNumerator = BN.max(
now.sub(
BN.max(new BN(userAccount.lastFuelBonusUpdateTs), FUEL_START_TS)
),
ZERO
);
if (fuelBonusNumerator.gt(ZERO)) {
for (const spotPosition of this.getActiveSpotPositions()) {
const spotMarketAccount: SpotMarketAccount =
this.driftClient.getSpotMarketAccount(spotPosition.marketIndex);
const tokenAmount = this.getTokenAmount(spotPosition.marketIndex);
const oraclePriceData = this.getOracleDataForSpotMarket(
spotPosition.marketIndex
);
const twap5min = calculateLiveOracleTwap(
spotMarketAccount.historicalOracleData,
oraclePriceData,
now,
FIVE_MINUTE // 5MIN
);
const strictOraclePrice = new StrictOraclePrice(
oraclePriceData.price,
twap5min
);
const signedTokenValue = getStrictTokenValue(
tokenAmount,
spotMarketAccount.decimals,
strictOraclePrice
);
if (signedTokenValue.gt(ZERO)) {
result.depositFuel = result.depositFuel.add(
calculateSpotFuelBonus(
spotMarketAccount,
signedTokenValue,
fuelBonusNumerator
)
);
} else {
result.borrowFuel = result.borrowFuel.add(
calculateSpotFuelBonus(
spotMarketAccount,
signedTokenValue,
fuelBonusNumerator
)
);
}
}
for (const perpPosition of this.getActivePerpPositions()) {
const oraclePriceData = this.getMMOracleDataForPerpMarket(
perpPosition.marketIndex
);
const perpMarketAccount = this.driftClient.getPerpMarketAccount(
perpPosition.marketIndex
);
const baseAssetValue = this.getPerpPositionValue(
perpPosition.marketIndex,
oraclePriceData,
false
);
result.positionFuel = result.positionFuel.add(
calculatePerpFuelBonus(
perpMarketAccount,
baseAssetValue,
fuelBonusNumerator
)
);
}
}
}
result.insuranceFuel = userStats.getInsuranceFuelBonus(
now,
includeSettled,
includeUnsettled
);
return result;
}
public getSpotMarketAssetAndLiabilityValue(
marketIndex?: number,
marginCategory?: MarginCategory,
liquidationBuffer?: BN,
includeOpenOrders?: boolean,
strict = false,
now?: BN
): { totalAssetValue: BN; totalLiabilityValue: BN } {
now = now || new BN(new Date().getTime() / 1000);
let netQuoteValue = ZERO;
let totalAssetValue = ZERO;
let totalLiabilityValue = ZERO;
for (const spotPosition of this.getUserAccount().spotPositions) {
const countForBase =
marketIndex === undefined || spotPosition.marketIndex === marketIndex;
const countForQuote =
marketIndex === undefined ||
marketIndex === QUOTE_SPOT_MARKET_INDEX ||
(includeOpenOrders && spotPosition.openOrders !== 0);
if (
isSpotPositionAvailable(spotPosition) ||
(!countForBase && !countForQuote)
) {
continue;
}
const spotMarketAccount: SpotMarketAccount =
this.driftClient.getSpotMarketAccount(spotPosition.marketIndex);
const oraclePriceData = this.getOracleDataForSpotMarket(
spotPosition.marketIndex
);
let twap5min;
if (strict) {
twap5min = calculateLiveOracleTwap(
spotMarketAccount.historicalOracleData,
oraclePriceData,
now,
FIVE_MINUTE // 5MIN
);
}
const strictOraclePrice = new StrictOraclePrice(
oraclePriceData.price,
twap5min
);
if (
spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX &&
countForQuote
) {
const tokenAmount = getSignedTokenAmount(
getTokenAmount(
spotPosition.scaledBalance,
spotMarketAccount,
spotPosition.balanceType
),
spotPosition.balanceType
);
if (isVariant(spotPosition.balanceType, 'borrow')) {
const weightedTokenValue = this.getSpotLiabilityValue(
tokenAmount,
strictOraclePrice,
spotMarketAccount,
marginCategory,
liquidationBuffer
).abs();
netQuoteValue = netQuoteValue.sub(weightedTokenValue);
} else {
const weightedTokenValue = this.getSpotAssetValue(
tokenAmount,
strictOraclePrice,
spotMarketAccount,
marginCategory
);
netQuoteValue = netQuoteValue.add(weightedTokenValue);
}
continue;
}
if (!includeOpenOrders && countForBase) {
if (isVariant(spotPosition.balanceType, 'borrow')) {
const tokenAmount = getSignedTokenAmount(
getTokenAmount(
spotPosition.scaledBalance,
spotMarketAccount,
spotPosition.balanceType
),
SpotBalanceType.BORROW
);
const liabilityValue = this.getSpotLiabilityValue(
tokenAmount,
strictOraclePrice,
spotMarketAccount,
marginCategory,
liquidationBuffer
).abs();
totalLiabilityValue = totalLiabilityValue.add(liabilityValue);
continue;
} else {
const tokenAmount = getTokenAmount(
spotPosition.scaledBalance,
spotMarketAccount,
spotPosition.balanceType
);
const assetValue = this.getSpotAssetValue(
tokenAmount,
strictOraclePrice,
spotMarketAccount,
marginCategory
);
totalAssetValue = totalAssetValue.add(assetValue);
continue;
}
}
const {
tokenAmount: worstCaseTokenAmount,
ordersValue: worstCaseQuoteTokenAmount,
} = getWorstCaseTokenAmounts(
spotPosition,
spotMarketAccount,
strictOraclePrice,
marginCategory,
this.getUserAccount().maxMarginRatio
);
if (worstCaseTokenAmount.gt(ZERO) && countForBase) {
const baseAssetValue = this.getSpotAssetValue(
worstCaseTokenAmount,
strictOraclePrice,
spotMarketAccount,
marginCategory
);
totalAssetValue = totalAssetValue.add(baseAssetValue);
}
if (worstCaseTokenAmount.lt(ZERO) && countForBase) {
const baseLiabilityValue = this.getSpotLiabilityValue(
worstCaseTokenAmount,
strictOraclePrice,
spotMarketAccount,
marginCategory,
liquidationBuffer
).abs();
totalLiabilityValue = totalLiabilityValue.add(baseLiabilityValue);
}
if (worstCaseQuoteTokenAmount.gt(ZERO) && countForQuote) {
netQuoteValue = netQuoteValue.add(worstCaseQuoteTokenAmount);
}
if (worstCaseQuoteTokenAmount.lt(ZERO) && countForQuote) {
let weight = SPOT_MARKET_WEIGHT_PRECISION;
if (marginCategory === 'Initial') {
weight = BN.max(weight, new BN(this.getUserAccount().maxMarginRatio));
}
const weightedTokenValue = worstCaseQuoteTokenAmount
.abs()
.mul(weight)
.div(SPOT_MARKET_WEIGHT_PRECISION);
netQuoteValue = netQuoteValue.sub(weightedTokenValue);
}
totalLiabilityValue = totalLiabilityValue.add(
new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)
);
}
if (marketIndex === undefined || marketIndex === QUOTE_SPOT_MARKET_INDEX) {
if (netQuoteValue.gt(ZERO)) {
totalAssetValue = totalAssetValue.add(netQuoteValue);
} else {
totalLiabilityValue = totalLiabilityValue.add(netQuoteValue.abs());
}
}
return { totalAssetValue, totalLiabilityValue };
}
public getSpotMarketLiabilityValue(
marketIndex?: number,
marginCategory?: MarginCategory,
liquidationBuffer?: BN,
includeOpenOrders?: boolean,
strict = false,
now?: BN
): BN {
const { totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(
marketIndex,
marginCategory,
liquidationBuffer,
includeOpenOrders,
strict,
now
);
return totalLiabilityValue;
}
getSpotLiabilityValue(
tokenAmount: BN,
strictOraclePrice: StrictOraclePrice,
spotMarketAccount: SpotMarketAccount,
marginCategory?: MarginCategory,
liquidationBuffer?: BN
): BN {
return getSpotLiabilityValue(
tokenAmount,
strictOraclePrice,
spotMarketAccount,
this.getUserAccount().maxMarginRatio,
marginCategory,
liquidationBuffer
);
}
public getSpotMarketAssetValue(
marketIndex?: number,
marginCategory?: MarginCategory,
includeOpenOrders?: boolean,
strict = false,
now?: BN
): BN {
const { totalAssetValue } = this.getSpotMarketAssetAndLiabilityValue(
marketIndex,
marginCategory,
undefined,
includeOpenOrders,
strict,
now
);
return totalAssetValue;
}
getSpotAssetValue(
tokenAmount: BN,
strictOraclePrice: StrictOraclePrice,
spotMarketAccount: SpotMarketAccount,
marginCategory?: MarginCategory
): BN {
return getSpotAssetValue(
tokenAmount,
strictOraclePrice,
spotMarketAccount,
this.getUserAccount().maxMarginRatio,
marginCategory
);
}
public getSpotPositionValue(
marketIndex: number,
marginCategory?: MarginCategory,
includeOpenOrders?: boolean,
strict = false,
now?: BN
): BN {
const { totalAssetValue, totalLiabilityValue } =
this.getSpotMarketAssetAndLiabilityValue(
marketIndex,
marginCategory,
undefined,
includeOpenOrders,
strict,
now
);
return totalAssetValue.sub(totalLiabilityValue);
}
public getNetSpotMarketValue(withWeightMarginCategory?: MarginCategory): BN {
const { totalAssetValue, totalLiabilityValue } =
this.getSpotMarketAssetAndLiabilityValue(
undefined,
withWeightMarginCategory
);
return totalAssetValue.sub(totalLiabilityValue);
}
/**
* calculates TotalCollateral: collateral + unrealized pnl
* @returns : Precision QUOTE_PRECISION
*/
public getTotalCollateral(
marginCategory: MarginCategory = 'Initial',
strict = false,
includeOpenOrders = true,
liquidationBuffer?: BN
): BN {
return this.getSpotMarketAssetValue(
undefined,
marginCategory,
includeOpenOrders,
strict
).add(
this.getUnrealizedPNL(
true,
undefined,
marginCategory,
strict,
liquidationBuffer
)
);
}
public getLiquidationBuffer(): BN | undefined {
// if user being liq'd, can continue to be liq'd until total collateral above the margin requirement plus buffer
let liquidationBuffer = undefined;
if (this.isBeingLiquidated()) {
liquidationBuffer = new BN(
this.driftClient.getStateAccount().liquidationMarginBufferRatio
);
}
return liquidationBuffer;
}
/**
* calculates User Health by comparing total collateral and maint. margin requirement
* @returns : number (value from [0, 100])
*/
public getHealth(): number {
if (this.isBeingLiquidated()) {
return 0;
}
const totalCollateral = this.getTotalCollateral('Maintenance');
const maintenanceMarginReq = this.getMaintenanceMarginRequirement();
let health: number;
if (maintenanceMarginReq.eq(ZERO) && totalCollateral.gte(ZERO)) {
health = 100;
} else if (totalCollateral.lte(ZERO)) {
health = 0;
} else {
health = Math.round(
Math.min(
100,
Math.max(
0,
(1 - maintenanceMarginReq.toNumber() / totalCollateral.toNumber()) *
100
)
)
);
}
return health;
}
calculateWeightedPerpPositionLiability(
perpPosition: PerpPosition,
marginCategory?: MarginCategory,
liquidationBuffer?: BN,
includeOpenOrders?: boolean,
strict = false,
enteringHighLeverage = undefined
): BN {
const market = this.driftClient.getPerpMarketAccount(
perpPosition.marketIndex
);
let valuationPrice = this.getOracleDataForPerpMarket(
market.marketIndex
).price;
if (isVariant(market.status, 'settlement')) {
valuationPrice = market.expiryPrice;
}
let baseAssetAmount: BN;
let liabilityValue;
if (includeOpenOrders) {
const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } =
calculateWorstCasePerpLiabilityValue(
perpPosition,
market,
valuationPrice
);
baseAssetAmount = worstCaseBaseAssetAmount;
liabilityValue = worstCaseLiabilityValue;
} else {
baseAssetAmount = perpPosition.baseAssetAmount;
liabilityValue = calculatePerpLiabilityValue(
baseAssetAmount,
valuationPrice,
isVariant(market.contractType, 'prediction')
);
}
if (marginCategory) {
const userCustomMargin = this.getUserAccount().maxMarginRatio;
let marginRatio = new BN(
calculateMarketMarginRatio(
market,
baseAssetAmount.abs(),
marginCategory,
enteringHighLeverage === false
? Math.max(market.marginRatioInitial, userCustomMargin)
: userCustomMargin,
this.isHighLeverageMode(marginCategory) ||
enteringHighLeverage === true
)
);
if (liquidationBuffer !== undefined) {
marginRatio = marginRatio.add(liquidationBuffer);
}
if (isVariant(market.status, 'settlement')) {
marginRatio = ZERO;
}
const quoteSpotMarket = this.driftClient.getSpotMarketAccount(
market.quoteSpotMarketIndex
);
const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(
QUOTE_SPOT_MARKET_INDEX
);
let quotePrice;
if (strict) {
quotePrice = BN.max(
quoteOraclePriceData.price,
quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min
);
} else {
quotePrice = quoteOraclePriceData.price;
}
liabilityValue = liabilityValue
.mul(quotePrice)
.div(PRICE_PRECISION)
.mul(marginRatio)
.div(MARGIN_PRECISION);
if (includeOpenOrders) {
liabilityValue = liabilityValue.add(
new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)
);
}
}
return liabilityValue;
}
/**
* calculates position value of a single perp market in margin system
* @returns : Precision QUOTE_PRECISION
*/
public getPerpMarketLiabilityValue(
marketIndex: number,
marginCategory?: MarginCategory,
liquidationBuffer?: BN,
includeOpenOrders?: boolean,
strict = false
): BN {
const perpPosition = this.getPerpPosition(marketIndex);
return this.calculateWeightedPerpPositionLiability(
perpPosition,
marginCategory,
liquidationBuffer,
includeOpenOrders,
strict
);
}
/**
* calculates sum of position value across all positions in margin system
* @returns : Precision QUOTE_PRECISION
*/
getTotalPerpPositionLiability(
marginCategory?: MarginCategory,
liquidationBuffer?: BN,
includeOpenOrders?: boolean,
strict = false,
enteringHighLeverage = undefined
): BN {
return this.getActivePerpPositions().reduce(
(totalPerpValue, perpPosition) => {
const baseAssetValue = this.calculateWeightedPerpPositionLiability(
perpPosition,
marginCategory,
liquidationBuffer,
includeOpenOrders,
strict,
enteringHighLeverage
);
return totalPerpValue.add(baseAssetValue);
},
ZERO
);
}
/**
* calculates position value based on oracle
* @returns : Precision QUOTE_PRECISION
*/
public getPerpPositionValue(
marketIndex: number,
oraclePriceData: Pick<OraclePriceData, 'price'>,
includeOpenOrders = false
): BN {
const userPosition = this.getPerpPositionOrEmpty(marketIndex);
const market = this.driftClient.getPerpMarketAccount(
userPosition.marketIndex
);
return calculateBaseAssetValueWithOracle(
market,
userPosition,
oraclePriceData,
includeOpenOrders
);
}
/**
* calculates position liabiltiy value in margin system
* @returns : Precision QUOTE_PRECISION
*/
public getPerpLiabilityValue(
marketIndex: number,
oraclePriceData: OraclePriceData,
includeOpenOrders = false
): BN {
const userPosition = this.getPerpPositionOrEmpty(marketIndex);
const market = this.driftClient.getPerpMarketAccount(
userPosition.marketIndex
);
if (includeOpenOrders) {
return calculateWorstCasePerpLiabilityValue(
userPosition,
market,
oraclePriceData.price
).worstCaseLiabilityValue;
} else {
return calculatePerpLiabilityValue(
userPosition.baseAssetAmount,
oraclePriceData.price,
isVariant(market.contractType, 'prediction')
);
}
}
public getPositionSide(
currentPosition: Pick<PerpPosition, 'baseAssetAmount'>
): PositionDirection | undefined {
if (currentPosition.baseAssetAmount.gt(ZERO)) {
return PositionDirection.LONG;
} else if (currentPosition.baseAssetAmount.lt(ZERO)) {
return PositionDirection.SHORT;
} else {
return undefined;
}
}
/**
* calculates average exit price (optionally for closing up to 100% of position)
* @returns : Precision PRICE_PRECISION
*/
public getPositionEstimatedExitPriceAndPnl(
position: PerpPosition,
amountToClose?: BN,
useAMMClose = false
): [BN, BN] {
const market = this.driftClient.getPerpMarketAccount(position.marketIndex);
const entryPrice = calculateEntryPrice(position);
const oraclePriceData = this.getMMOracleDataForPerpMarket(
position.marketIndex
);
if (amountToClose) {
if (amountToClose.eq(ZERO)) {
return [calculateReservePrice(market, oraclePriceData), ZERO];
}
position = {
baseAssetAmount: amountToClose,
lastCumulativeFundingRate: position.lastCumulativeFundingRate,
marketIndex: position.marketIndex,
quoteAssetAmount: position.quoteAssetAmount,
} as PerpPosition;
}
let baseAssetValue: BN;
if (useAMMClose) {
baseAssetValue = calculateBaseAssetValue(
market,
position,
oraclePriceData
);
} else {
baseAssetValue = calculateBaseAssetValueWithOracle(
market,
position,
oraclePriceData
);
}
if (position.baseAssetAmount.eq(ZERO)) {
return [ZERO, ZERO];
}
const exitPrice = baseAssetValue
.mul(AMM_TO_QUOTE_PRECISION_RATIO)
.mul(PRICE_PRECISION)
.div(position.baseAssetAmount.abs());
const pnlPerBase = exitPrice.sub(entryPrice);
const pnl = pnlPerBase
.mul(position.baseAssetAmount)
.div(PRICE_PRECISION)
.div(AMM_TO_QUOTE_PRECISION_RATIO);
return [exitPrice, pnl];
}
/**
* calculates current user leverage which is (total liability size) / (net asset value)
* @returns : Precision TEN_THOUSAND
*/
public getLeverage(includeOpenOrders = true): BN {
return this.calculateLeverageFromComponents(
this.getLeverageComponents(includeOpenOrders)
);
}
calculateLeverageFromComponents({
perpLiabilityValue,
perpPnl,
spotAssetValue,
spotLiabilityValue,
}: {
perpLiabilityValue: BN;
perpPnl: BN;
spotAssetValue: BN;
spotLiabilityValue: BN;
}): BN {
const totalLiabilityValue = perpLiabilityValue.add(spotLiabilityValue);
const totalAssetValue = spotAssetValue.add(perpPnl);
const netAssetValue = totalAssetValue.sub(spotLiabilityValue);
if (netAssetValue.eq(ZERO)) {
return ZERO;
}
return totalLiabilityValue.mul(TEN_THOUSAND).div(netAssetValue);
}
getLeverageComponents(
includeOpenOrders = true,
marginCategory: MarginCategory = undefined
): {
perpLiabilityValue: BN;
perpPnl: BN;
spotAssetValue: BN;
spotLiabilityValue: BN;
} {
const perpLiability = this.getTotalPerpPositionLiability(
marginCategory,
undefined,
includeOpenOrders
);
const perpPnl = this.getUnrealizedPNL(true, undefined, marginCategory);
const {
totalAssetValue: spotAssetValue,
totalLiabilityValue: spotLiabilityValue,
} = this.getSpotMarketAssetAndLiabilityValue(
undefined,
marginCategory,
undefined,
includeOpenOrders
);
return {
perpLiabilityValue: perpLiability,
perpPnl,
spotAssetValue,
spotLiabilityValue,
};
}
isDustDepositPosition(spotMarketAccount: SpotMarketAccount): boolean {
const marketIndex = spotMarketAccount.marketIndex;
const spotPosition = this.getSpotPosition(spotMarketAccount.marketIndex);
if (isSpotPositionAvailable(spotPosition)) {
return false;
}
const depositAmount = this.getTokenAmount(spotMarketAccount.marketIndex);
if (depositAmount.lte(ZERO)) {
return false;
}
const oraclePriceData = this.getOracleDataForSpotMarket(marketIndex);
const strictOraclePrice = new StrictOraclePrice(
oraclePriceData.price,
oraclePriceData.twap
);
const balanceValue = this.getSpotAssetValue(
depositAmount,
strictOraclePrice,
spotMarketAccount
);
if (balanceValue.lt(DUST_POSITION_SIZE)) {
return true;
}
return false;
}
getSpotMarketAccountsWithDustPosition() {
const spotMarketAccounts = this.driftClient.getSpotMarketAccounts();
const dustPositionAccounts: SpotMarketAccount[] = [];
for (const spotMarketAccount of spotMarketAccounts) {
const isDust = this.isDustDepositPosition(spotMarketAccount);
if (isDust) {
dustPositionAccounts.push(spotMarketAccount);
}
}
return dustPositionAccounts;
}
getTotalLiabilityValue(marginCategory?: MarginCategory): BN {
return this.getTotalPerpPositionLiability(
marginCategory,
undefined,
true
).add(
this.getSpotMarketLiabilityValue(
undefined,
marginCategory,
undefined,
true
)
);
}
getTotalAssetValue(marginCategory?: MarginCategory): BN {
return this.getSpotMarketAssetValue(undefined, marginCategory, true).add(
this.getUnrealizedPNL(true, undefined, marginCategory)
);
}
getNetUsdValue(): BN {
const netSpotValue = this.getNetSpotMarketValue();
const unrealizedPnl = this.getUnrealizedPNL(true, undefined, undefined);
return netSpotValue.add(unrealizedPnl);
}
/**
* Calculates the all time P&L of the user.
*
* Net withdraws + Net spot market value + Net unrealized P&L -
*/
getTotalAllTimePnl(): BN {
const netUsdValue = this.getNetUsdValue();
const totalDeposits = this.getUserAccount().totalDeposits;
const totalWithdraws = this.getUserAccount().totalWithdraws;
const totalPnl = netUsdValue.add(totalWithdraws).sub(totalDeposits);
return totalPnl;
}
/**
* calculates max allowable leverage exceeding hitting requirement category
* for large sizes where imf factor activates, result is a lower bound
* @param marginCategory {Initial, Maintenance}
* @param isLp if calculating max leveraging for adding lp, need to add buffer
* @param enterHighLeverageMode can pass this as true to calculate max leverage if the user was to enter high leverage mode
* @returns : Precision TEN_THOUSAND
*/
public getMaxLeverageForPerp(
perpMarketIndex: number,
_marginCategory: MarginCategory = 'Initial',
isLp = false,
enterHighLeverageMode = undefined
): BN {
const market = this.driftClient.getPerpMarketAccount(perpMarketIndex);
const marketPrice =
this.driftClient.getOracleDataForPerpMarket(perpMarketIndex).price;
const { perpLiabilityValue, perpPnl, spotAssetValue, spotLiabilityValue } =
this.getLeverageComponents();
const totalAssetValue = spotAssetValue.add(perpPnl);
const netAssetValue = totalAssetValue.sub(spotLiabilityValue);
if (netAssetValue.eq(ZERO)) {
return ZERO;
}
const totalLiabilityValue = perpLiabilityValue.add(spotLiabilityValue);
const lpBuffer = isLp
? marketPrice.mul(market.amm.orderStepSize).div(AMM_RESERVE_PRECISION)
: ZERO;
// absolute max fesible size (upper bound)
const maxSizeQuote = BN.max(
BN.min(
this.getMaxTradeSizeUSDCForPerp(
perpMarketIndex,
PositionDirection.LONG,
false,
enterHighLeverageMode || this.isHighLeverageMode('Initial')
).tradeSize,
this.getMaxTradeSizeUSDCForPerp(
perpMarketIndex,
PositionDirection.SHORT,
false,
enterHighLeverageMode || this.isHighLeverageMode('Initial')
).tradeSize
).sub(lpBuffer),
ZERO
);
return totalLiabilityValue
.add(maxSizeQuote)
.mul(TEN_THOUSAND)
.div(netAssetValue);
}
/**
* calculates max allowable leverage exceeding hitting requirement category
* @param spotMarketIndex
* @param direction
* @returns : Precision TEN_THOUSAND
*/
public getMaxLeverageForSpot(
spotMarketIndex: number,
direction: PositionDirection
): BN {
const { perpLiabilityValue, perpPnl, spotAssetValue, spotLiabilityValue } =
this.getLeverageComponents();
const totalLiabilityValue = perpLiabilityValue.add(spotLiabilityValue);
const totalAssetValue = spotAssetValue.add(perpPnl);
const netAssetValue = totalAssetValue.sub(spotLiabilityValue);
if (netAssetValue.eq(ZERO)) {
return ZERO;
}
const currentQuoteAssetValue = this.getSpotMarketAssetValue(
QUOTE_SPOT_MARKET_INDEX
);
const currentQuoteLiabilityValue = this.getSpotMarketLiabilityValue(
QUOTE_SPOT_MARKET_INDEX
);
const currentQuoteValue = currentQuoteAssetValue.sub(
currentQuoteLiabilityValue
);
const currentSpotMarketAssetValue =
this.getSpotMarketAssetValue(spotMarketIndex);
const currentSpotMarketLiabilityValue =
this.getSpotMarketLiabilityValue(spotMarketIndex);
const currentSpotMarketNetValue = currentSpotMarketAssetValue.sub(
currentSpotMarketLiabilityValue
);
const tradeQuoteAmount = this.getMaxTradeSizeUSDCForSpot(
spotMarketIndex,
direction,
currentQuoteAssetValue,
currentSpotMarketNetValue
);
let assetValueToAdd = ZERO;
let liabilityValueToAdd = ZERO;
const newQuoteNetValue = isVariant(direction, 'short')
? currentQuoteValue.add(tradeQuoteAmount)
: currentQuoteValue.sub(tradeQuoteAmount);
const newQuoteAssetValue = BN.max(newQuoteNetValue, ZERO);
const newQuoteLiabilityValue = BN.min(newQuoteNetValue, ZERO).abs();
assetValueToAdd = assetValueToAdd.add(
newQuoteAssetValue.sub(currentQuoteAssetValue)
);
liabilityValueToAdd = liabilityValueToAdd.add(
newQuoteLiabilityValue.sub(currentQuoteLiabilityValue)
);
const newSpotMarketNetValue = isVariant(direction, 'long')
? currentSpotMarketNetValue.add(tradeQuoteAmount)
: currentSpotMarketNetValue.sub(tradeQuoteAmount);
const newSpotMarketAssetValue = BN.max(newSpotMarketNetValue, ZERO);
const newSpotMarketLiabilityValue = BN.min(
newSpotMarketNetValue,
ZERO
).abs();
assetValueToAdd = assetValueToAdd.add(
newSpotMarketAssetValue.sub(currentSpotMarketAssetValue)
);
liabilityValueToAdd = liabilityValueToAdd.add(
newSpotMarketLiabilityValue.sub(currentSpotMarketLiabilityValue)
);
const finalTotalAssetValue = totalAssetValue.add(assetValueToAdd);
const finalTotalSpotLiability = spotLiabilityValue.add(liabilityValueToAdd);
const finalTotalLiabilityValue =
totalLiabilityValue.add(liabilityValueToAdd);
const finalNetAssetValue = finalTotalAssetValue.sub(
finalTotalSpotLiability
);
return finalTotalLiabilityValue.mul(TEN_THOUSAND).div(finalNetAssetValue);
}
/**
* calculates margin ratio: 1 / leverage
* @returns : Precision TEN_THOUSAND
*/
public getMarginRatio(): BN {
const { perpLiabilityValue, perpPnl, spotAssetValue, spotLiabilityValue } =
this.getLeverageComponents();
const totalLiabilityValue = perpLiabilityValue.add(spotLiabilityValue);
const totalAssetValue = spotAssetValue.add(perpPnl);
if (totalLiabilityValue.eq(ZERO)) {
return BN_MAX;
}
const netAssetValue = totalAssetValue.sub(spotLiabilityValue);
return netAssetValue.mul(TEN_THOUSAND).div(totalLiabilityValue);
}
public canBeLiquidated(): {
canBeLiquidated: boolean;
marginRequirement: BN;
totalCollateral: BN;
} {
const liquidationBuffer = this.getLiquidationBuffer();
const totalCollateral = this.getTotalCollateral(
'Maintenance',
undefined,
undefined,
liquidationBuffer
);
const marginRequirement =
this.getMaintenanceMarginRequirement(liquidationBuffer);
const canBeLiquidated = totalCollateral.lt(marginRequirement);
return {
canBeLiquidated,
marginRequirement,
totalCollateral,
};
}
public isBeingLiquidated(): boolean {
return (
(this.getUserAccount().status &
(UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) >
0
);
}
public hasStatus(status: UserStatus): boolean {
return (this.getUserAccount().status & status) > 0;
}
public isBankrupt(): boolean {
return (this.getUserAccount().status & UserStatus.BANKRUPT) > 0;
}
public isHighLeverageMode(marginCategory: MarginCategory): boolean {
return (
isVariant(this.getUserAccount().marginMode, 'highLeverage') ||
(marginCategory === 'Maintenance' &&
isVariant(this.getUserAccount().marginMode, 'highLeverageMaintenance'))
);
}
/**
* Checks if any user position cumulative funding differs from respective market cumulative funding
* @returns
*/
public needsToSettleFundingPayment(): boolean {
for (const userPosition of this.getUserAccount().perpPositions) {
if (userPosition.baseAssetAmount.eq(ZERO)) {
continue;
}
const market = this.driftClient.getPerpMarketAccount(
userPosition.marketIndex
);
if (
market.amm.cumulativeFundingRateLong.eq(
userPosition.lastCumulativeFundingRate
) ||
market.amm.cumulativeFundingRateShort.eq(
userPosition.lastCumulativeFundingRate
)
) {
continue;
}
return true;
}
return false;
}
/**
* Calculate the liquidation price of a spot position
* @param marketIndex
* @returns Precision : PRICE_PRECISION
*/
public spotLiquidationPrice(
marketIndex: number,
positionBaseSizeChange: BN = ZERO
): BN {
const currentSpotPosition = this.getSpotPosition(marketIndex);
if (!currentSpotPosition) {
return new BN(-1);
}
const totalCollateral = this.getTotalCollateral('Maintenance');
const maintenanceMarginRequirement = this.getMaintenanceMarg