UNPKG

@drift-labs/sdk-browser

Version:
1,901 lines (1,688 loc) 105 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_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