UNPKG

@drift-labs/sdk

Version:
1,882 lines (1,683 loc) • 113 kB
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_RESERVE_PRECISION_EXP, AMM_TO_QUOTE_PRECISION_RATIO, BASE_PRECISION, BN_MAX, DUST_POSITION_SIZE, 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_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 { calculateAssetWeight, calculateLiabilityWeight, calculateWithdrawLimit, getSpotAssetValue, getSpotLiabilityValue, getTokenAmount, } from './math/spotBalance'; import { calculateMarketOpenBidAsk } from './math/amm'; import { calculateBaseAssetValueWithOracle, calculateCollateralDepositRequiredForTrade, calculateMarginUSDCRequiredForTrade, 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, 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 { 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 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 = calculateUnsettledFundingPnl(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, enterHighLeverageMode = undefined ): BN { const perpPosition = this.getPerpPositionWithLPSettle( marketIndex, undefined, true )[0]; 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 ); } getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount( marketIndex: number, freeCollateral: BN, baseAssetAmount: BN, enterHighLeverageMode = undefined ): BN { const marginRatio = calculateMarketMarginRatio( this.driftClient.getPerpMarketAccount(marketIndex), baseAssetAmount, 'Initial', this.getUserAccount().maxMarginRatio, 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) || !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, 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.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)); } 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.getOracleDataForPerpMarket( 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 ); 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; } 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) ); if (perpPosition.lpShares.gt(ZERO)) { liabilityValue = liabilityValue.add( BN.max( QUOTE_PRECISION, valuationPrice .mul(market.amm.orderStepSize) .mul(QUOTE_PRECISION) .div(AMM_RESERVE_PRECISION) .div(PRICE_PRECISION) ) ); } } } 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.getPerpPositionWithLPSettle( marketIndex, undefined, false, true )[0] || this.getEmptyPosition(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.getPerpPositionWithLPSettle( marketIndex, undefined, false, true )[0] || this.getEmptyPosition(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.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.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().t