UNPKG

@ox-fun/drift-sdk

Version:

SDK for Drift Protocol

1,869 lines (1,650 loc) 97.6 kB
import { PublicKey } from '@solana/web3.js'; import { EventEmitter } from 'events'; import StrictEventEmitter from 'strict-event-emitter-types'; import { DriftClient } from './driftClient'; import { HealthComponents, isVariant, MarginCategory, Order, PerpMarketAccount, PerpPosition, SpotPosition, UserAccount, UserStatus, UserStatsAccount, } from './types'; import { calculateEntryPrice, positionIsAvailable } from './math/position'; import { AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_EXP, AMM_TO_QUOTE_PRECISION_RATIO, BASE_PRECISION, BN_MAX, FIVE_MINUTE, MARGIN_PRECISION, ONE, OPEN_ORDER_MARGIN_REQUIREMENT, PRICE_PRECISION, QUOTE_PRECISION, QUOTE_PRECISION_EXP, QUOTE_SPOT_MARKET_INDEX, SPOT_MARKET_WEIGHT_PRECISION, TEN, TEN_THOUSAND, TWO, ZERO, } from './constants/numericConstants'; import { DataAndSlot, UserAccountEvents, UserAccountSubscriber, } from './accounts/types'; import { BigNum, BN, calculateBaseAssetValue, calculateMarketMarginRatio, calculatePositionFundingPNL, calculatePositionPNL, calculateReservePrice, calculateSpotMarketMarginRatio, calculateUnrealizedAssetWeight, divCeil, getBalance, getSignedTokenAmount, getStrictTokenValue, getTokenValue, MarketType, PositionDirection, sigNum, SpotBalanceType, SpotMarketAccount, } from '.'; import { calculateAssetWeight, calculateLiabilityWeight, calculateWithdrawLimit, getTokenAmount, } from './math/spotBalance'; import { calculateMarketOpenBidAsk } from './math/amm'; import { calculateBaseAssetValueWithOracle, calculateWorstCaseBaseAssetAmount, } from './math/margin'; import { 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 } from './math/oracles'; import { getPerpMarketTierNumber, getSpotMarketTierNumber } from './math/tiers'; import { StrictOraclePrice } from './oracles/strictOraclePrice'; 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.program, config.userAccountPublicKey, config.accountSubscription.accountLoader ); } else if (config.accountSubscription?.type === 'custom') { this.accountSubscriber = config.accountSubscription.userAccountSubscriber; } else { this.accountSubscriber = new WebSocketUserAccountSubscriber( config.driftClient.program, config.userAccountPublicKey, config.accountSubscription?.resubTimeoutMs, 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 userAccount.perpPositions.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 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, }; } 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 [lpOpenBids, lpOpenAsks] = this.getLPBidAsks(marketIndex); const totalOpenBids = lpOpenBids.add(position.openBids); const totalOpenAsks = lpOpenAsks.add(position.openAsks); return [totalOpenBids, totalOpenAsks]; } /** * calculates the open bids and asks for an lp * optionally pass in lpShares to see what bid/asks a user *would* take on * @returns : lp open bids * @returns : lp open asks */ public getLPBidAsks(marketIndex: number, lpShares?: BN): [BN, BN] { const position = this.getPerpPosition(marketIndex); const lpSharesToCalc = lpShares ?? position?.lpShares; if (!lpSharesToCalc || lpSharesToCalc.eq(ZERO)) { return [ZERO, ZERO]; } const market = this.driftClient.getPerpMarketAccount(marketIndex); const [marketOpenBids, marketOpenAsks] = calculateMarketOpenBidAsk( market.amm.baseAssetReserve, market.amm.minBaseAssetReserve, market.amm.maxBaseAssetReserve, market.amm.orderStepSize ); const lpOpenBids = marketOpenBids.mul(lpSharesToCalc).div(market.amm.sqrtK); const lpOpenAsks = marketOpenAsks.mul(lpSharesToCalc).div(market.amm.sqrtK); return [lpOpenBids, lpOpenAsks]; } /** * calculates the market position if the lp position was settled * @returns : the settled userPosition * @returns : the dust base asset amount (ie, < stepsize) * @returns : pnl from settle */ public getPerpPositionWithLPSettle( marketIndex: number, originalPosition?: PerpPosition, burnLpShares = false, includeRemainderInBaseAmount = false ): [PerpPosition, BN, BN] { originalPosition = originalPosition ?? this.getPerpPosition(marketIndex) ?? this.getEmptyPosition(marketIndex); if (originalPosition.lpShares.eq(ZERO)) { return [originalPosition, ZERO, ZERO]; } const position = this.getClonedPosition(originalPosition); const market = this.driftClient.getPerpMarketAccount(position.marketIndex); if (market.amm.perLpBase != position.perLpBase) { // perLpBase = 1 => per 10 LP shares, perLpBase = -1 => per 0.1 LP shares const expoDiff = market.amm.perLpBase - position.perLpBase; const marketPerLpRebaseScalar = new BN(10 ** Math.abs(expoDiff)); if (expoDiff > 0) { position.lastBaseAssetAmountPerLp = position.lastBaseAssetAmountPerLp.mul(marketPerLpRebaseScalar); position.lastQuoteAssetAmountPerLp = position.lastQuoteAssetAmountPerLp.mul(marketPerLpRebaseScalar); } else { position.lastBaseAssetAmountPerLp = position.lastBaseAssetAmountPerLp.div(marketPerLpRebaseScalar); position.lastQuoteAssetAmountPerLp = position.lastQuoteAssetAmountPerLp.div(marketPerLpRebaseScalar); } position.perLpBase = position.perLpBase + expoDiff; } const nShares = position.lpShares; // incorp unsettled funding on pre settled position const quoteFundingPnl = calculatePositionFundingPNL(market, position); let baseUnit = AMM_RESERVE_PRECISION; if (market.amm.perLpBase == position.perLpBase) { if ( position.perLpBase >= 0 && position.perLpBase <= AMM_RESERVE_PRECISION_EXP.toNumber() ) { const marketPerLpRebase = new BN(10 ** market.amm.perLpBase); baseUnit = baseUnit.mul(marketPerLpRebase); } else if ( position.perLpBase < 0 && position.perLpBase >= -AMM_RESERVE_PRECISION_EXP.toNumber() ) { const marketPerLpRebase = new BN(10 ** Math.abs(market.amm.perLpBase)); baseUnit = baseUnit.div(marketPerLpRebase); } else { throw 'cannot calc'; } } else { throw 'market.amm.perLpBase != position.perLpBase'; } const deltaBaa = market.amm.baseAssetAmountPerLp .sub(position.lastBaseAssetAmountPerLp) .mul(nShares) .div(baseUnit); const deltaQaa = market.amm.quoteAssetAmountPerLp .sub(position.lastQuoteAssetAmountPerLp) .mul(nShares) .div(baseUnit); function sign(v: BN) { return v.isNeg() ? new BN(-1) : new BN(1); } function standardize(amount: BN, stepSize: BN) { const remainder = amount.abs().mod(stepSize).mul(sign(amount)); const standardizedAmount = amount.sub(remainder); return [standardizedAmount, remainder]; } const [standardizedBaa, remainderBaa] = standardize( deltaBaa, market.amm.orderStepSize ); position.remainderBaseAssetAmount += remainderBaa.toNumber(); if ( Math.abs(position.remainderBaseAssetAmount) > market.amm.orderStepSize.toNumber() ) { const [newStandardizedBaa, newRemainderBaa] = standardize( new BN(position.remainderBaseAssetAmount), market.amm.orderStepSize ); position.baseAssetAmount = position.baseAssetAmount.add(newStandardizedBaa); position.remainderBaseAssetAmount = newRemainderBaa.toNumber(); } let dustBaseAssetValue = ZERO; if (burnLpShares && position.remainderBaseAssetAmount != 0) { const oraclePriceData = this.driftClient.getOracleDataForPerpMarket( position.marketIndex ); dustBaseAssetValue = new BN(Math.abs(position.remainderBaseAssetAmount)) .mul(oraclePriceData.price) .div(AMM_RESERVE_PRECISION) .add(ONE); } let updateType; if (position.baseAssetAmount.eq(ZERO)) { updateType = 'open'; } else if (sign(position.baseAssetAmount).eq(sign(deltaBaa))) { updateType = 'increase'; } else if (position.baseAssetAmount.abs().gt(deltaBaa.abs())) { updateType = 'reduce'; } else if (position.baseAssetAmount.abs().eq(deltaBaa.abs())) { updateType = 'close'; } else { updateType = 'flip'; } let newQuoteEntry; let pnl; if (updateType == 'open' || updateType == 'increase') { newQuoteEntry = position.quoteEntryAmount.add(deltaQaa); pnl = ZERO; } else if (updateType == 'reduce' || updateType == 'close') { newQuoteEntry = position.quoteEntryAmount.sub( position.quoteEntryAmount .mul(deltaBaa.abs()) .div(position.baseAssetAmount.abs()) ); pnl = position.quoteEntryAmount.sub(newQuoteEntry).add(deltaQaa); } else { newQuoteEntry = deltaQaa.sub( deltaQaa.mul(position.baseAssetAmount.abs()).div(deltaBaa.abs()) ); pnl = position.quoteEntryAmount.add(deltaQaa.sub(newQuoteEntry)); } position.quoteEntryAmount = newQuoteEntry; position.baseAssetAmount = position.baseAssetAmount.add(standardizedBaa); position.quoteAssetAmount = position.quoteAssetAmount .add(deltaQaa) .add(quoteFundingPnl) .sub(dustBaseAssetValue); position.quoteBreakEvenAmount = position.quoteBreakEvenAmount .add(deltaQaa) .add(quoteFundingPnl) .sub(dustBaseAssetValue); // update open bids/asks const [marketOpenBids, marketOpenAsks] = calculateMarketOpenBidAsk( market.amm.baseAssetReserve, market.amm.minBaseAssetReserve, market.amm.maxBaseAssetReserve, market.amm.orderStepSize ); const lpOpenBids = marketOpenBids .mul(position.lpShares) .div(market.amm.sqrtK); const lpOpenAsks = marketOpenAsks .mul(position.lpShares) .div(market.amm.sqrtK); position.openBids = lpOpenBids.add(position.openBids); position.openAsks = lpOpenAsks.add(position.openAsks); // eliminate counting funding on settled position if (position.baseAssetAmount.gt(ZERO)) { position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateLong; } else if (position.baseAssetAmount.lt(ZERO)) { position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateShort; } else { position.lastCumulativeFundingRate = ZERO; } const remainderBeforeRemoval = new BN(position.remainderBaseAssetAmount); if (includeRemainderInBaseAmount) { position.baseAssetAmount = position.baseAssetAmount.add( remainderBeforeRemoval ); position.remainderBaseAssetAmount = 0; } return [position, remainderBeforeRemoval, pnl]; } /** * calculates Buying Power = free collateral / initial margin ratio * @returns : Precision QUOTE_PRECISION */ public getPerpBuyingPower(marketIndex: number, collateralBuffer = ZERO): BN { const perpPosition = this.getPerpPositionWithLPSettle( marketIndex, undefined, true )[0]; const worstCaseBaseAssetAmount = perpPosition ? calculateWorstCaseBaseAssetAmount(perpPosition) : ZERO; const freeCollateral = this.getFreeCollateral().sub(collateralBuffer); return this.getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount( marketIndex, freeCollateral, worstCaseBaseAssetAmount ); } getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount( marketIndex: number, freeCollateral: BN, baseAssetAmount: BN ): BN { const marginRatio = calculateMarketMarginRatio( this.driftClient.getPerpMarketAccount(marketIndex), baseAssetAmount, 'Initial', this.getUserAccount().maxMarginRatio ); 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'): BN { const totalCollateral = this.getTotalCollateral(marginCategory, true); const marginRequirement = marginCategory === 'Initial' ? this.getInitialMarginRequirement() : 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 ): BN { return this.getTotalPerpPositionValue( marginCategory, liquidationBuffer, true, strict ).add( this.getSpotMarketLiabilityValue( undefined, marginCategory, liquidationBuffer, true, strict ) ); } /** * @returns The initial margin requirement in USDC. : QUOTE_PRECISION */ public getInitialMarginRequirement(): BN { return this.getMarginRequirement('Initial', undefined, true); } /** * @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) || !pos.lpShares.eq(ZERO) ); } 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 ): 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.getOracleDataForPerpMarket( market.marketIndex ); const quoteSpotMarket = this.driftClient.getSpotMarketAccount( market.quoteSpotMarketIndex ); const quoteOraclePriceData = this.getOracleDataForSpotMarket( market.quoteSpotMarketIndex ); if (perpPosition.lpShares.gt(ZERO)) { perpPosition = this.getPerpPositionWithLPSettle( perpPosition.marketIndex, undefined, !!withWeightMarginCategory )[0]; } 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)); } } 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 ? pos.marketIndex === marketIndex : true ) .reduce((pnl, perpPosition) => { const market = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex ); return pnl.add(calculatePositionFundingPNL(market, perpPosition)); }, ZERO); } 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 { let liabilityValue = getStrictTokenValue( tokenAmount, spotMarketAccount.decimals, strictOraclePrice ); if (marginCategory !== undefined) { let weight = calculateLiabilityWeight( tokenAmount, spotMarketAccount, marginCategory ); if ( marginCategory === 'Initial' && spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX ) { weight = BN.max( weight, SPOT_MARKET_WEIGHT_PRECISION.addn( this.getUserAccount().maxMarginRatio ) ); } if (liquidationBuffer !== undefined) { weight = weight.add(liquidationBuffer); } liabilityValue = liabilityValue .mul(weight) .div(SPOT_MARKET_WEIGHT_PRECISION); } return liabilityValue; } 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 { let assetValue = getStrictTokenValue( tokenAmount, spotMarketAccount.decimals, strictOraclePrice ); if (marginCategory !== undefined) { let weight = calculateAssetWeight( tokenAmount, strictOraclePrice.current, spotMarketAccount, marginCategory ); if ( marginCategory === 'Initial' && spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX ) { const userCustomAssetWeight = BN.max( ZERO, SPOT_MARKET_WEIGHT_PRECISION.subn( this.getUserAccount().maxMarginRatio ) ); weight = BN.min(weight, userCustomAssetWeight); } assetValue = assetValue.mul(weight).div(SPOT_MARKET_WEIGHT_PRECISION); } return assetValue; } 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 ): BN { return this.getSpotMarketAssetValue( undefined, marginCategory, true, strict ).add(this.getUnrealizedPNL(true, undefined, marginCategory, strict)); } /** * 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; } calculateWeightedPerpPositionValue( perpPosition: PerpPosition, marginCategory?: MarginCategory, liquidationBuffer?: BN, includeOpenOrders?: boolean, strict = false ): BN { const market = this.driftClient.getPerpMarketAccount( perpPosition.marketIndex ); if (perpPosition.lpShares.gt(ZERO)) { // is an lp, clone so we dont mutate the position perpPosition = this.getPerpPositionWithLPSettle( market.marketIndex, this.getClonedPosition(perpPosition), !!marginCategory )[0]; } let valuationPrice = this.getOracleDataForPerpMarket( market.marketIndex ).price; if (isVariant(market.status, 'settlement')) { valuationPrice = market.expiryPrice; } const baseAssetAmount = includeOpenOrders ? calculateWorstCaseBaseAssetAmount(perpPosition) : perpPosition.baseAssetAmount; let baseAssetValue = baseAssetAmount .abs() .mul(valuationPrice) .div(BASE_PRECISION); if (marginCategory) { let marginRatio = new BN( calculateMarketMarginRatio( market, baseAssetAmount.abs(), marginCategory, this.getUserAccount().maxMarginRatio ) ); 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.getOraclePriceDataAndSlot( quoteSpotMarket.oracle ).data; let quotePrice; if (strict) { quotePrice = BN.max( quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min ); } else { quotePrice = quoteOraclePriceData.price; } baseAssetValue = baseAssetValue .mul(quotePrice) .div(PRICE_PRECISION) .mul(marginRatio) .div(MARGIN_PRECISION); if (includeOpenOrders) { baseAssetValue = baseAssetValue.add( new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT) ); if (perpPosition.lpShares.gt(ZERO)) { baseAssetValue = baseAssetValue.add( BN.max( QUOTE_PRECISION, valuationPrice .mul(market.amm.orderStepSize) .mul(QUOTE_PRECISION) .div(AMM_RESERVE_PRECISION) .div(PRICE_PRECISION) ) ); } } } return baseAssetValue; } /** * 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.calculateWeightedPerpPositionValue( perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict ); } /** * calculates sum of position value across all positions in margin system * @returns : Precision QUOTE_PRECISION */ getTotalPerpPositionValue( marginCategory?: MarginCategory, liquidationBuffer?: BN, includeOpenOrders?: boolean, strict = false ): BN { return this.getActivePerpPositions().reduce( (totalPerpValue, perpPosition) => { const baseAssetValue = this.calculateWeightedPerpPositionValue( perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict ); return totalPerpValue.add(baseAssetValue); }, ZERO ); } /** * calculates position value in margin system * @returns : Precision QUOTE_PRECISION */ public getPerpPositionValue( marketIndex: number, oraclePriceData: OraclePriceData, includeOpenOrders = false ): BN { const userPosition = this.getPerpPositionWithLPSettle( marketIndex, undefined, false, true )[0] || this.getEmptyPosition(marketIndex); const market = this.driftClient.getPerpMarketAccount( userPosition.marketIndex ); return calculateBaseAssetValueWithOracle( market, userPosition, oraclePriceData, includeOpenOrders ); } 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.getOracleDataForPerpMarket( 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.getTotalPerpPositionValue( 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, }; } getTotalLiabilityValue(marginCategory?: MarginCategory): BN { return this.getTotalPerpPositionValue(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 * @returns : Precision TEN_THOUSAND */ public getMaxLeverageForPerp( perpMarketIndex: number, marginCategory: MarginCategory = 'Initial', isLp = false ): 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; const freeCollateral = this.getFreeCollateral().sub(lpBuffer); let rawMarginRatio; switch (marginCategory) { case 'Initial': rawMarginRatio = Math.max( market.marginRatioInitial, this.getUserAccount().maxMarginRatio ); break; case 'Maintenance': rawMarginRatio = market.marginRatioMaintenance; break; default: rawMarginRatio = market.marginRatioInitial; break; } // absolute max fesible size (upper bound) const maxSize = BN.max( ZERO, freeCollateral .mul(MARGIN_PRECISION) .div(new BN(rawMarginRatio)) .mul(PRICE_PRECISION) .div(marketPrice) ); // margin ratio incorporting upper bound on size let marginRatio = calculateMarketMarginRatio( market, maxSize, marginCategory, this.getUserAccount().maxMarginRatio ); // use more fesible size since imf factor activated let attempts = 0; while (marginRatio > rawMarginRatio + 1e-4 && attempts < 10) { // more fesible size (upper bound) const targetSize = BN.max( ZERO, freeCollateral .mul(MARGIN_PRECISION) .div(new BN(marginRatio)) .mul(PRICE_PRECISION) .div(marketPrice) ); // margin ratio incorporting more fesible target size marginRatio = calculateMarketMarginRatio( market, targetSize, marginCategory, this.getUserAccount().maxMarginRatio ); attempts += 1; } // how much more liabilities can be opened w remaining free collateral const additionalLiabilities = freeCollateral .mul(MARGIN_PRECISION) .div(new BN(marginRatio)); return totalLiabilityValue .add(additionalLiabilities) .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 totalCollateral = this.getTotalCollateral('Maintenance'); // 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 ); } 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 isBankrupt(): boolean { return (this.getUserAccount().status & UserStatus.BANKRUPT) > 0; } /** * 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.lastC