UNPKG

@drift-labs/sdk

Version:
893 lines • 115 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.User = void 0; const types_1 = require("./types"); const position_1 = require("./math/position"); const numericConstants_1 = require("./constants/numericConstants"); const bigNum_1 = require("./factory/bigNum"); const anchor_1 = require("@coral-xyz/anchor"); const position_2 = require("./math/position"); const market_1 = require("./math/market"); const margin_1 = require("./math/margin"); const spotMarket_1 = require("./math/spotMarket"); const utils_1 = require("./math/utils"); const spotBalance_1 = require("./math/spotBalance"); const trade_1 = require("./math/trade"); const types_2 = require("./types"); const orders_1 = require("./math/orders"); const websocketProgramUserAccountSubscriber_1 = require("./accounts/websocketProgramUserAccountSubscriber"); const spotBalance_2 = require("./math/spotBalance"); const margin_2 = require("./math/margin"); const pollingUserAccountSubscriber_1 = require("./accounts/pollingUserAccountSubscriber"); const webSocketUserAccountSubscriber_1 = require("./accounts/webSocketUserAccountSubscriber"); const spotPosition_1 = require("./math/spotPosition"); const oracles_1 = require("./math/oracles"); const tiers_1 = require("./math/tiers"); const strictOraclePrice_1 = require("./oracles/strictOraclePrice"); const fuel_1 = require("./math/fuel"); const grpcUserAccountSubscriber_1 = require("./accounts/grpcUserAccountSubscriber"); class User { get isSubscribed() { return this._isSubscribed && this.accountSubscriber.isSubscribed; } set isSubscribed(val) { this._isSubscribed = val; } constructor(config) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; this._isSubscribed = false; this.driftClient = config.driftClient; this.userAccountPublicKey = config.userAccountPublicKey; if (((_a = config.accountSubscription) === null || _a === void 0 ? void 0 : _a.type) === 'polling') { this.accountSubscriber = new pollingUserAccountSubscriber_1.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 (((_b = config.accountSubscription) === null || _b === void 0 ? void 0 : _b.type) === 'custom') { this.accountSubscriber = config.accountSubscription.userAccountSubscriber; } else if (((_c = config.accountSubscription) === null || _c === void 0 ? void 0 : _c.type) === 'grpc') { this.accountSubscriber = new grpcUserAccountSubscriber_1.grpcUserAccountSubscriber(config.accountSubscription.grpcConfigs, config.driftClient.program, config.userAccountPublicKey, { resubTimeoutMs: (_d = config.accountSubscription) === null || _d === void 0 ? void 0 : _d.resubTimeoutMs, logResubMessages: (_e = config.accountSubscription) === null || _e === void 0 ? void 0 : _e.logResubMessages, }); } else { if (((_f = config.accountSubscription) === null || _f === void 0 ? void 0 : _f.type) === 'websocket' && ((_g = config.accountSubscription) === null || _g === void 0 ? void 0 : _g.programUserAccountSubscriber)) { this.accountSubscriber = new websocketProgramUserAccountSubscriber_1.WebSocketProgramUserAccountSubscriber(config.driftClient.program, config.userAccountPublicKey, config.accountSubscription.programUserAccountSubscriber); } else { this.accountSubscriber = new webSocketUserAccountSubscriber_1.WebSocketUserAccountSubscriber(config.driftClient.program, config.userAccountPublicKey, { resubTimeoutMs: (_h = config.accountSubscription) === null || _h === void 0 ? void 0 : _h.resubTimeoutMs, logResubMessages: (_j = config.accountSubscription) === null || _j === void 0 ? void 0 : _j.logResubMessages, }, (_k = config.accountSubscription) === null || _k === void 0 ? void 0 : _k.commitment); } } this.eventEmitter = this.accountSubscriber.eventEmitter; } /** * Subscribe to User state accounts * @returns SusbcriptionSuccess result */ async subscribe(userAccount) { this.isSubscribed = await this.accountSubscriber.subscribe(userAccount); return this.isSubscribed; } /** * Forces the accountSubscriber to fetch account updates from rpc */ async fetchAccounts() { await this.accountSubscriber.fetch(); } async unsubscribe() { await this.accountSubscriber.unsubscribe(); this.isSubscribed = false; } getUserAccount() { return this.accountSubscriber.getUserAccountAndSlot().data; } async forceGetUserAccount() { await this.fetchAccounts(); return this.accountSubscriber.getUserAccountAndSlot().data; } getUserAccountAndSlot() { return this.accountSubscriber.getUserAccountAndSlot(); } getPerpPositionForUserAccount(userAccount, marketIndex) { 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 */ getPerpPosition(marketIndex) { const userAccount = this.getUserAccount(); return this.getPerpPositionForUserAccount(userAccount, marketIndex); } getPerpPositionOrEmpty(marketIndex) { var _a; const userAccount = this.getUserAccount(); return ((_a = this.getPerpPositionForUserAccount(userAccount, marketIndex)) !== null && _a !== void 0 ? _a : this.getEmptyPosition(marketIndex)); } getPerpPositionAndSlot(marketIndex) { const userAccount = this.getUserAccountAndSlot(); const perpPosition = this.getPerpPositionForUserAccount(userAccount.data, marketIndex); return { data: perpPosition, slot: userAccount.slot, }; } getSpotPositionForUserAccount(userAccount, marketIndex) { 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 */ getSpotPosition(marketIndex) { const userAccount = this.getUserAccount(); return this.getSpotPositionForUserAccount(userAccount, marketIndex); } getSpotPositionAndSlot(marketIndex) { const userAccount = this.getUserAccountAndSlot(); const spotPosition = this.getSpotPositionForUserAccount(userAccount.data, marketIndex); return { data: spotPosition, slot: userAccount.slot, }; } getEmptySpotPosition(marketIndex) { return { marketIndex, scaledBalance: numericConstants_1.ZERO, balanceType: types_2.SpotBalanceType.DEPOSIT, cumulativeDeposits: numericConstants_1.ZERO, openAsks: numericConstants_1.ZERO, openBids: numericConstants_1.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 */ getTokenAmount(marketIndex) { const spotPosition = this.getSpotPosition(marketIndex); if (spotPosition === undefined) { return numericConstants_1.ZERO; } const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex); return (0, spotBalance_1.getSignedTokenAmount)((0, spotBalance_2.getTokenAmount)(spotPosition.scaledBalance, spotMarket, spotPosition.balanceType), spotPosition.balanceType); } getEmptyPosition(marketIndex) { return { baseAssetAmount: numericConstants_1.ZERO, remainderBaseAssetAmount: 0, lastCumulativeFundingRate: numericConstants_1.ZERO, marketIndex, quoteAssetAmount: numericConstants_1.ZERO, quoteEntryAmount: numericConstants_1.ZERO, quoteBreakEvenAmount: numericConstants_1.ZERO, openOrders: 0, openBids: numericConstants_1.ZERO, openAsks: numericConstants_1.ZERO, settledPnl: numericConstants_1.ZERO, lpShares: numericConstants_1.ZERO, lastBaseAssetAmountPerLp: numericConstants_1.ZERO, lastQuoteAssetAmountPerLp: numericConstants_1.ZERO, perLpBase: 0, maxMarginRatio: 0, }; } getClonedPosition(position) { const clonedPosition = Object.assign({}, position); return clonedPosition; } getOrderForUserAccount(userAccount, orderId) { return userAccount.orders.find((order) => order.orderId === orderId); } /** * @param orderId * @returns Order */ getOrder(orderId) { const userAccount = this.getUserAccount(); return this.getOrderForUserAccount(userAccount, orderId); } getOrderAndSlot(orderId) { const userAccount = this.getUserAccountAndSlot(); const order = this.getOrderForUserAccount(userAccount.data, orderId); return { data: order, slot: userAccount.slot, }; } getOrderByUserIdForUserAccount(userAccount, userOrderId) { return userAccount.orders.find((order) => order.userOrderId === userOrderId); } /** * @param userOrderId * @returns Order */ getOrderByUserOrderId(userOrderId) { const userAccount = this.getUserAccount(); return this.getOrderByUserIdForUserAccount(userAccount, userOrderId); } getOrderByUserOrderIdAndSlot(userOrderId) { const userAccount = this.getUserAccountAndSlot(); const order = this.getOrderByUserIdForUserAccount(userAccount.data, userOrderId); return { data: order, slot: userAccount.slot, }; } getOpenOrdersForUserAccount(userAccount) { return userAccount === null || userAccount === void 0 ? void 0 : userAccount.orders.filter((order) => (0, types_1.isVariant)(order.status, 'open')); } getOpenOrders() { const userAccount = this.getUserAccount(); return this.getOpenOrdersForUserAccount(userAccount); } getOpenOrdersAndSlot() { const userAccount = this.getUserAccountAndSlot(); const openOrders = this.getOpenOrdersForUserAccount(userAccount.data); return { data: openOrders, slot: userAccount.slot, }; } getUserAccountPublicKey() { return this.userAccountPublicKey; } async exists() { 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 */ getPerpBidAsks(marketIndex) { 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 */ getPerpBuyingPower(marketIndex, collateralBuffer = numericConstants_1.ZERO, enterHighLeverageMode = undefined) { const perpPosition = this.getPerpPositionOrEmpty(marketIndex); const perpMarket = this.driftClient.getPerpMarketAccount(marketIndex); const oraclePriceData = this.getOracleDataForPerpMarket(marketIndex); const worstCaseBaseAssetAmount = perpPosition ? (0, margin_2.calculateWorstCaseBaseAssetAmount)(perpPosition, perpMarket, oraclePriceData.price) : numericConstants_1.ZERO; const freeCollateral = this.getFreeCollateral('Initial', enterHighLeverageMode).sub(collateralBuffer); return this.getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount(marketIndex, freeCollateral, worstCaseBaseAssetAmount, enterHighLeverageMode, perpPosition); } getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount(marketIndex, freeCollateral, baseAssetAmount, enterHighLeverageMode = undefined, perpPosition) { var _a; const userCustomMargin = Math.max((_a = perpPosition === null || perpPosition === void 0 ? void 0 : perpPosition.maxMarginRatio) !== null && _a !== void 0 ? _a : 0, this.getUserAccount().maxMarginRatio); const marginRatio = (0, market_1.calculateMarketMarginRatio)(this.driftClient.getPerpMarketAccount(marketIndex), baseAssetAmount, 'Initial', userCustomMargin, enterHighLeverageMode || this.isHighLeverageMode('Initial')); return freeCollateral.mul(numericConstants_1.MARGIN_PRECISION).div(new anchor_1.BN(marginRatio)); } /** * calculates Free Collateral = Total collateral - margin requirement * @returns : Precision QUOTE_PRECISION */ getFreeCollateral(marginCategory = 'Initial', enterHighLeverageMode = undefined) { const totalCollateral = this.getTotalCollateral(marginCategory, true); const marginRequirement = marginCategory === 'Initial' ? this.getInitialMarginRequirement(enterHighLeverageMode) : this.getMaintenanceMarginRequirement(); const freeCollateral = totalCollateral.sub(marginRequirement); return freeCollateral.gte(numericConstants_1.ZERO) ? freeCollateral : numericConstants_1.ZERO; } /** * @returns The margin requirement of a certain type (Initial or Maintenance) in USDC. : QUOTE_PRECISION */ getMarginRequirement(marginCategory, liquidationBuffer, strict = false, includeOpenOrders = true, enteringHighLeverage = undefined) { 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 */ getInitialMarginRequirement(enterHighLeverageMode = undefined) { return this.getMarginRequirement('Initial', undefined, true, undefined, enterHighLeverageMode); } /** * @returns The maintenance margin requirement in USDC. : QUOTE_PRECISION */ getMaintenanceMarginRequirement(liquidationBuffer) { return this.getMarginRequirement('Maintenance', liquidationBuffer); } getActivePerpPositionsForUserAccount(userAccount) { return userAccount.perpPositions.filter((pos) => !pos.baseAssetAmount.eq(numericConstants_1.ZERO) || !pos.quoteAssetAmount.eq(numericConstants_1.ZERO) || !(pos.openOrders == 0)); } getActivePerpPositions() { const userAccount = this.getUserAccount(); return this.getActivePerpPositionsForUserAccount(userAccount); } getActivePerpPositionsAndSlot() { const userAccount = this.getUserAccountAndSlot(); const positions = this.getActivePerpPositionsForUserAccount(userAccount.data); return { data: positions, slot: userAccount.slot, }; } getActiveSpotPositionsForUserAccount(userAccount) { return userAccount.spotPositions.filter((pos) => !(0, spotPosition_1.isSpotPositionAvailable)(pos)); } getActiveSpotPositions() { const userAccount = this.getUserAccount(); return this.getActiveSpotPositionsForUserAccount(userAccount); } getActiveSpotPositionsAndSlot() { 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 */ getUnrealizedPNL(withFunding, marketIndex, withWeightMarginCategory, strict = false, liquidationBuffer) { 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 = (0, position_2.calculatePositionPNL)(market, perpPosition, withFunding, oraclePriceData); let quotePrice; if (strict && positionUnrealizedPnl.gt(numericConstants_1.ZERO)) { quotePrice = anchor_1.BN.min(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min); } else if (strict && positionUnrealizedPnl.lt(numericConstants_1.ZERO)) { quotePrice = anchor_1.BN.max(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min); } else { quotePrice = quoteOraclePriceData.price; } positionUnrealizedPnl = positionUnrealizedPnl .mul(quotePrice) .div(numericConstants_1.PRICE_PRECISION); if (withWeightMarginCategory !== undefined) { if (positionUnrealizedPnl.gt(numericConstants_1.ZERO)) { positionUnrealizedPnl = positionUnrealizedPnl .mul((0, market_1.calculateUnrealizedAssetWeight)(market, quoteSpotMarket, positionUnrealizedPnl, withWeightMarginCategory, oraclePriceData)) .div(new anchor_1.BN(numericConstants_1.SPOT_MARKET_WEIGHT_PRECISION)); } if (liquidationBuffer && positionUnrealizedPnl.lt(numericConstants_1.ZERO)) { positionUnrealizedPnl = positionUnrealizedPnl.add(positionUnrealizedPnl.mul(liquidationBuffer).div(numericConstants_1.MARGIN_PRECISION)); } } return unrealizedPnl.add(positionUnrealizedPnl); }, numericConstants_1.ZERO); } /** * calculates unrealized funding payment pnl * @returns : Precision QUOTE_PRECISION */ getUnrealizedFundingPNL(marketIndex) { 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((0, position_1.calculateUnsettledFundingPnl)(market, perpPosition)); }, numericConstants_1.ZERO); } getFuelBonus(now, includeSettled = true, includeUnsettled = true, givenUserStats) { const userAccount = this.getUserAccount(); const result = { insuranceFuel: numericConstants_1.ZERO, takerFuel: numericConstants_1.ZERO, makerFuel: numericConstants_1.ZERO, depositFuel: numericConstants_1.ZERO, borrowFuel: numericConstants_1.ZERO, positionFuel: numericConstants_1.ZERO, }; const userStats = givenUserStats !== null && givenUserStats !== void 0 ? givenUserStats : this.driftClient.getUserStats(); const userStatsAccount = userStats.getAccount(); if (includeSettled) { result.takerFuel = result.takerFuel.add(new anchor_1.BN(userStatsAccount.fuelTaker)); result.makerFuel = result.makerFuel.add(new anchor_1.BN(userStatsAccount.fuelMaker)); result.depositFuel = result.depositFuel.add(new anchor_1.BN(userStatsAccount.fuelDeposits)); result.borrowFuel = result.borrowFuel.add(new anchor_1.BN(userStatsAccount.fuelBorrows)); result.positionFuel = result.positionFuel.add(new anchor_1.BN(userStatsAccount.fuelPositions)); } if (includeUnsettled) { const fuelBonusNumerator = anchor_1.BN.max(now.sub(anchor_1.BN.max(new anchor_1.BN(userAccount.lastFuelBonusUpdateTs), numericConstants_1.FUEL_START_TS)), numericConstants_1.ZERO); if (fuelBonusNumerator.gt(numericConstants_1.ZERO)) { for (const spotPosition of this.getActiveSpotPositions()) { const spotMarketAccount = this.driftClient.getSpotMarketAccount(spotPosition.marketIndex); const tokenAmount = this.getTokenAmount(spotPosition.marketIndex); const oraclePriceData = this.getOracleDataForSpotMarket(spotPosition.marketIndex); const twap5min = (0, oracles_1.calculateLiveOracleTwap)(spotMarketAccount.historicalOracleData, oraclePriceData, now, numericConstants_1.FIVE_MINUTE // 5MIN ); const strictOraclePrice = new strictOraclePrice_1.StrictOraclePrice(oraclePriceData.price, twap5min); const signedTokenValue = (0, spotBalance_1.getStrictTokenValue)(tokenAmount, spotMarketAccount.decimals, strictOraclePrice); if (signedTokenValue.gt(numericConstants_1.ZERO)) { result.depositFuel = result.depositFuel.add((0, fuel_1.calculateSpotFuelBonus)(spotMarketAccount, signedTokenValue, fuelBonusNumerator)); } else { result.borrowFuel = result.borrowFuel.add((0, fuel_1.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((0, fuel_1.calculatePerpFuelBonus)(perpMarketAccount, baseAssetValue, fuelBonusNumerator)); } } } result.insuranceFuel = userStats.getInsuranceFuelBonus(now, includeSettled, includeUnsettled); return result; } getSpotMarketAssetAndLiabilityValue(marketIndex, marginCategory, liquidationBuffer, includeOpenOrders, strict = false, now) { now = now || new anchor_1.BN(new Date().getTime() / 1000); let netQuoteValue = numericConstants_1.ZERO; let totalAssetValue = numericConstants_1.ZERO; let totalLiabilityValue = numericConstants_1.ZERO; for (const spotPosition of this.getUserAccount().spotPositions) { const countForBase = marketIndex === undefined || spotPosition.marketIndex === marketIndex; const countForQuote = marketIndex === undefined || marketIndex === numericConstants_1.QUOTE_SPOT_MARKET_INDEX || (includeOpenOrders && spotPosition.openOrders !== 0); if ((0, spotPosition_1.isSpotPositionAvailable)(spotPosition) || (!countForBase && !countForQuote)) { continue; } const spotMarketAccount = this.driftClient.getSpotMarketAccount(spotPosition.marketIndex); const oraclePriceData = this.getOracleDataForSpotMarket(spotPosition.marketIndex); let twap5min; if (strict) { twap5min = (0, oracles_1.calculateLiveOracleTwap)(spotMarketAccount.historicalOracleData, oraclePriceData, now, numericConstants_1.FIVE_MINUTE // 5MIN ); } const strictOraclePrice = new strictOraclePrice_1.StrictOraclePrice(oraclePriceData.price, twap5min); if (spotPosition.marketIndex === numericConstants_1.QUOTE_SPOT_MARKET_INDEX && countForQuote) { const tokenAmount = (0, spotBalance_1.getSignedTokenAmount)((0, spotBalance_2.getTokenAmount)(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType), spotPosition.balanceType); if ((0, types_1.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 ((0, types_1.isVariant)(spotPosition.balanceType, 'borrow')) { const tokenAmount = (0, spotBalance_1.getSignedTokenAmount)((0, spotBalance_2.getTokenAmount)(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType), types_2.SpotBalanceType.BORROW); const liabilityValue = this.getSpotLiabilityValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer).abs(); totalLiabilityValue = totalLiabilityValue.add(liabilityValue); continue; } else { const tokenAmount = (0, spotBalance_2.getTokenAmount)(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType); const assetValue = this.getSpotAssetValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory); totalAssetValue = totalAssetValue.add(assetValue); continue; } } const { tokenAmount: worstCaseTokenAmount, ordersValue: worstCaseQuoteTokenAmount, } = (0, spotPosition_1.getWorstCaseTokenAmounts)(spotPosition, spotMarketAccount, strictOraclePrice, marginCategory, this.getUserAccount().maxMarginRatio); if (worstCaseTokenAmount.gt(numericConstants_1.ZERO) && countForBase) { const baseAssetValue = this.getSpotAssetValue(worstCaseTokenAmount, strictOraclePrice, spotMarketAccount, marginCategory); totalAssetValue = totalAssetValue.add(baseAssetValue); } if (worstCaseTokenAmount.lt(numericConstants_1.ZERO) && countForBase) { const baseLiabilityValue = this.getSpotLiabilityValue(worstCaseTokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer).abs(); totalLiabilityValue = totalLiabilityValue.add(baseLiabilityValue); } if (worstCaseQuoteTokenAmount.gt(numericConstants_1.ZERO) && countForQuote) { netQuoteValue = netQuoteValue.add(worstCaseQuoteTokenAmount); } if (worstCaseQuoteTokenAmount.lt(numericConstants_1.ZERO) && countForQuote) { let weight = numericConstants_1.SPOT_MARKET_WEIGHT_PRECISION; if (marginCategory === 'Initial') { weight = anchor_1.BN.max(weight, new anchor_1.BN(this.getUserAccount().maxMarginRatio)); } const weightedTokenValue = worstCaseQuoteTokenAmount .abs() .mul(weight) .div(numericConstants_1.SPOT_MARKET_WEIGHT_PRECISION); netQuoteValue = netQuoteValue.sub(weightedTokenValue); } totalLiabilityValue = totalLiabilityValue.add(new anchor_1.BN(spotPosition.openOrders).mul(numericConstants_1.OPEN_ORDER_MARGIN_REQUIREMENT)); } if (marketIndex === undefined || marketIndex === numericConstants_1.QUOTE_SPOT_MARKET_INDEX) { if (netQuoteValue.gt(numericConstants_1.ZERO)) { totalAssetValue = totalAssetValue.add(netQuoteValue); } else { totalLiabilityValue = totalLiabilityValue.add(netQuoteValue.abs()); } } return { totalAssetValue, totalLiabilityValue }; } getSpotMarketLiabilityValue(marketIndex, marginCategory, liquidationBuffer, includeOpenOrders, strict = false, now) { const { totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(marketIndex, marginCategory, liquidationBuffer, includeOpenOrders, strict, now); return totalLiabilityValue; } getSpotLiabilityValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer) { return (0, spotBalance_2.getSpotLiabilityValue)(tokenAmount, strictOraclePrice, spotMarketAccount, this.getUserAccount().maxMarginRatio, marginCategory, liquidationBuffer); } getSpotMarketAssetValue(marketIndex, marginCategory, includeOpenOrders, strict = false, now) { const { totalAssetValue } = this.getSpotMarketAssetAndLiabilityValue(marketIndex, marginCategory, undefined, includeOpenOrders, strict, now); return totalAssetValue; } getSpotAssetValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory) { return (0, spotBalance_2.getSpotAssetValue)(tokenAmount, strictOraclePrice, spotMarketAccount, this.getUserAccount().maxMarginRatio, marginCategory); } getSpotPositionValue(marketIndex, marginCategory, includeOpenOrders, strict = false, now) { const { totalAssetValue, totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(marketIndex, marginCategory, undefined, includeOpenOrders, strict, now); return totalAssetValue.sub(totalLiabilityValue); } getNetSpotMarketValue(withWeightMarginCategory) { const { totalAssetValue, totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(undefined, withWeightMarginCategory); return totalAssetValue.sub(totalLiabilityValue); } /** * calculates TotalCollateral: collateral + unrealized pnl * @returns : Precision QUOTE_PRECISION */ getTotalCollateral(marginCategory = 'Initial', strict = false, includeOpenOrders = true, liquidationBuffer) { return this.getSpotMarketAssetValue(undefined, marginCategory, includeOpenOrders, strict).add(this.getUnrealizedPNL(true, undefined, marginCategory, strict, liquidationBuffer)); } getLiquidationBuffer() { // 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 anchor_1.BN(this.driftClient.getStateAccount().liquidationMarginBufferRatio); } return liquidationBuffer; } /** * calculates User Health by comparing total collateral and maint. margin requirement * @returns : number (value from [0, 100]) */ getHealth() { if (this.isBeingLiquidated()) { return 0; } const totalCollateral = this.getTotalCollateral('Maintenance'); const maintenanceMarginReq = this.getMaintenanceMarginRequirement(); let health; if (maintenanceMarginReq.eq(numericConstants_1.ZERO) && totalCollateral.gte(numericConstants_1.ZERO)) { health = 100; } else if (totalCollateral.lte(numericConstants_1.ZERO)) { health = 0; } else { health = Math.round(Math.min(100, Math.max(0, (1 - maintenanceMarginReq.toNumber() / totalCollateral.toNumber()) * 100))); } return health; } calculateWeightedPerpPositionLiability(perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict = false, enteringHighLeverage = undefined) { const market = this.driftClient.getPerpMarketAccount(perpPosition.marketIndex); let valuationPrice = this.getOracleDataForPerpMarket(market.marketIndex).price; if ((0, types_1.isVariant)(market.status, 'settlement')) { valuationPrice = market.expiryPrice; } let baseAssetAmount; let liabilityValue; if (includeOpenOrders) { const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } = (0, margin_1.calculateWorstCasePerpLiabilityValue)(perpPosition, market, valuationPrice); baseAssetAmount = worstCaseBaseAssetAmount; liabilityValue = worstCaseLiabilityValue; } else { baseAssetAmount = perpPosition.baseAssetAmount; liabilityValue = (0, margin_1.calculatePerpLiabilityValue)(baseAssetAmount, valuationPrice, (0, types_1.isVariant)(market.contractType, 'prediction')); } if (marginCategory) { const userCustomMargin = this.getUserAccount().maxMarginRatio; let marginRatio = new anchor_1.BN((0, market_1.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 ((0, types_1.isVariant)(market.status, 'settlement')) { marginRatio = numericConstants_1.ZERO; } const quoteSpotMarket = this.driftClient.getSpotMarketAccount(market.quoteSpotMarketIndex); const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(numericConstants_1.QUOTE_SPOT_MARKET_INDEX); let quotePrice; if (strict) { quotePrice = anchor_1.BN.max(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min); } else { quotePrice = quoteOraclePriceData.price; } liabilityValue = liabilityValue .mul(quotePrice) .div(numericConstants_1.PRICE_PRECISION) .mul(marginRatio) .div(numericConstants_1.MARGIN_PRECISION); if (includeOpenOrders) { liabilityValue = liabilityValue.add(new anchor_1.BN(perpPosition.openOrders).mul(numericConstants_1.OPEN_ORDER_MARGIN_REQUIREMENT)); } } return liabilityValue; } /** * calculates position value of a single perp market in margin system * @returns : Precision QUOTE_PRECISION */ getPerpMarketLiabilityValue(marketIndex, marginCategory, liquidationBuffer, includeOpenOrders, strict = false) { 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, liquidationBuffer, includeOpenOrders, strict = false, enteringHighLeverage = undefined) { return this.getActivePerpPositions().reduce((totalPerpValue, perpPosition) => { const baseAssetValue = this.calculateWeightedPerpPositionLiability(perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict, enteringHighLeverage); return totalPerpValue.add(baseAssetValue); }, numericConstants_1.ZERO); } /** * calculates position value based on oracle * @returns : Precision QUOTE_PRECISION */ getPerpPositionValue(marketIndex, oraclePriceData, includeOpenOrders = false) { const userPosition = this.getPerpPositionOrEmpty(marketIndex); const market = this.driftClient.getPerpMarketAccount(userPosition.marketIndex); return (0, margin_2.calculateBaseAssetValueWithOracle)(market, userPosition, oraclePriceData, includeOpenOrders); } /** * calculates position liabiltiy value in margin system * @returns : Precision QUOTE_PRECISION */ getPerpLiabilityValue(marketIndex, oraclePriceData, includeOpenOrders = false) { const userPosition = this.getPerpPositionOrEmpty(marketIndex); const market = this.driftClient.getPerpMarketAccount(userPosition.marketIndex); if (includeOpenOrders) { return (0, margin_1.calculateWorstCasePerpLiabilityValue)(userPosition, market, oraclePriceData.price).worstCaseLiabilityValue; } else { return (0, margin_1.calculatePerpLiabilityValue)(userPosition.baseAssetAmount, oraclePriceData.price, (0, types_1.isVariant)(market.contractType, 'prediction')); } } getPositionSide(currentPosition) { if (currentPosition.baseAssetAmount.gt(numericConstants_1.ZERO)) { return types_2.PositionDirection.LONG; } else if (currentPosition.baseAssetAmount.lt(numericConstants_1.ZERO)) { return types_2.PositionDirection.SHORT; } else { return undefined; } } /** * calculates average exit price (optionally for closing up to 100% of position) * @returns : Precision PRICE_PRECISION */ getPositionEstimatedExitPriceAndPnl(position, amountToClose, useAMMClose = false) { const market = this.driftClient.getPerpMarketAccount(position.marketIndex); const entryPrice = (0, position_1.calculateEntryPrice)(position); const oraclePriceData = this.getMMOracleDataForPerpMarket(position.marketIndex); if (amountToClose) { if (amountToClose.eq(numericConstants_1.ZERO)) { return [(0, market_1.calculateReservePrice)(market, oraclePriceData), numericConstants_1.ZERO]; } position = { baseAssetAmount: amountToClose, lastCumulativeFundingRate: position.lastCumulativeFundingRate, marketIndex: position.marketIndex, quoteAssetAmount: position.quoteAssetAmount, }; } let baseAssetValue; if (useAMMClose) { baseAssetValue = (0, position_2.calculateBaseAssetValue)(market, position, oraclePriceData); } else { baseAssetValue = (0, margin_2.calculateBaseAssetValueWithOracle)(market, position, oraclePriceData); } if (position.baseAssetAmount.eq(numericConstants_1.ZERO)) { return [numericConstants_1.ZERO, numericConstants_1.ZERO]; } const exitPrice = baseAssetValue .mul(numericConstants_1.AMM_TO_QUOTE_PRECISION_RATIO) .mul(numericConstants_1.PRICE_PRECISION) .div(position.baseAssetAmount.abs()); const pnlPerBase = exitPrice.sub(entryPrice); const pnl = pnlPerBase .mul(position.baseAssetAmount) .div(numericConstants_1.PRICE_PRECISION) .div(numericConstants_1.AMM_TO_QUOTE_PRECISION_RATIO); return [exitPrice, pnl]; } /** * calculates current user leverage which is (total liability size) / (net asset value) * @returns : Precision TEN_THOUSAND */ getLeverage(includeOpenOrders = true) { return this.calculateLeverageFromComponents(this.getLeverageComponents(includeOpenOrders)); } calculateLeverageFromComponents({ perpLiabilityValue, perpPnl, spotAssetValue, spotLiabilityValue, }) { const totalLiabilityValue = perpLiabilityValue.add(spotLiabilityValue); const totalAssetValue = spotAssetValue.add(perpPnl); const netAssetValue = totalAssetValue.sub(spotLiabilityValue); if (netAssetValue.eq(numericConstants_1.ZERO)) { return numericConstants_1.ZERO; } return totalLiabilityValue.mul(numericConstants_1.TEN_THOUSAND).div(netAssetValue); } getLeverageComponents(includeOpenOrders = true, marginCategory = undefined) { 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) { const marketIndex = spotMarketAccount.marketIndex; const spotPosition = this.getSpotPosition(spotMarketAccount.marketIndex); if ((0, spotPosition_1.isSpotPositionAvailable)(spotPosition)) { return false; } const depositAmount = this.getTokenAmount(spotMarketAccount.marketIndex); if (depositAmount.lte(numericConstants_1.ZERO)) { return false; } const oraclePriceData = this.getOracleDataForSpotMarket(marketIndex); const strictOraclePrice = new strictOraclePrice_1.StrictOraclePrice(oraclePriceData.price, oraclePriceData.twap); const balanceValue = this.getSpotAssetValue(depositAmount, strictOraclePrice, spotMarketAccount); if (balanceValue.lt(numericConstants_1.DUST_POSITION_SIZE)) { return true; } return false; } getSpotMarketAccountsWithDustPosition() { const spotMarketAccounts = this.driftClient.getSpotMarketAccounts(); const dustPositionAccounts = []; for (const spotMarketAccount of spotMarketAccounts) { const isDust = this.isDustDepositPosition(spotMarketAccount); if (isDust) { dustPositionAccounts.push(spotMarketAccount); } } return dustPositionAccounts; } getTotalLiabilityValue(marginCategory) { return this.getTotalPerpPositionLiability(marginCategory, undefined, true).add(this.getSpotMarketLiabilityValue(undefined, marginCategory, undefined, true)); } getTotalAssetValue(marginCategory) { return this.getSpotMarketAssetValue(undefined, marginCategory, true).add(this.getUnrealizedPNL(true, undefined, marginCategory)); } getNetUsdValue() { 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() { 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 */ getMaxLeverageForPerp(perpMarketIndex, _marginCategory = 'Initial', isLp = false, enterHighLeverageMode = undefined) { 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(numericConstants_1.ZERO)) { return numericConstants_1.ZERO; } const totalLiabilityValue = perpLiabilityValue.add(spotLiabilityValue); const lpBuffer = isLp ? marketPrice.mul(market.amm.orderStepSize).div(numericConstants_1.AMM_RESERVE_PRECISION) : numericConstants_1.ZERO; // absolute max fesible size (upper bound) const maxSizeQuote = anchor_1.BN.max(anchor_1.BN.min(this.getMaxTradeSizeUSDCForPerp(perpMarketIndex, types_2.PositionDirection.LONG, false, enterHighLeverageMode || this.isHighLeverageMode('Initial')).tradeSize, this.getMaxTradeSizeUSDCForPerp(perpMarketIndex, types_2.PositionDirection.SHORT, false, enterHighLeverageMode || this.isHighLeverageMode('Initial')).tradeSize).sub(lpBuffer), numericConstants_1.ZERO); return totalLiabilityValue .add(maxSizeQuote) .mul(numericConstants_1.TEN_THOUSAND) .div(netAssetValue); } /** * calculates max allowable leverage exceeding hitting requirement category * @param spotMarketIndex * @param direction * @returns : Precision TEN_THOUSAND */ getMaxLeverageForSpot(spotMarketIndex, direction) { 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(numericConstants_1.ZERO)) { return numericConstants_1.ZERO; } const currentQuoteAssetValue = this.getSpotMarketAssetValue(numericConstants_1.QUOTE_SPOT_MARKET_INDEX); const currentQuoteLiabilityValue = this.getSpotMarketLiabilityValue(numericConstants_1.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 = numericConstants_1.ZERO; let liabilityValueToAdd = numericConstants_1.ZERO; const newQuoteNetValue = (0, types_1.isVariant)(direction, 'short') ? currentQuoteValue.add(tradeQuoteAmount) : currentQuoteValue.sub(tradeQuoteAmount); const newQuoteAssetValue = anchor_1.BN.max(newQuoteNetValue, numericConstants_1.ZERO); const newQuoteLiabilityValue = anchor_1.BN.min(newQuoteNetValue, numericConstants_1.ZERO).abs(); assetValueToAdd = assetValueToAdd.add(newQuoteAssetValue.sub(currentQuoteAssetValue)); liabilityValueToAdd = liabilityValueToAdd.add(newQuoteLiabilityValue.sub(currentQuoteLiabilityValue)); const newSpotMarketNetValue = (0, types_1.isVariant)(direction, 'long') ? currentSpotMarketNetValue.add(tradeQuoteAmount) : currentSpotMarketNetValue.sub(tradeQuoteAmount); const newSpotMarketAssetValue = anchor_1.BN.max(newSpotMarketNetValue, numericConstants_1.ZERO); const newSpotMarketLiabilityValue = anchor_1.BN.min(newSpotMarketNetValue, numericConstants_1.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(numericConstants_1.TEN_THOUSAND).div(finalNetAssetValue); } /** * calculates margin ratio: 1 / leverage * @returns : Precision TEN_THOUSAND */ getMarginRatio() { const { perpLiabilityValue, perpPnl, spotAssetValue, spotLiabilityValue } = this.getLeverageComponents(); const totalLiabilityValue = perpLiabilityValue.add(spotLiabilityValue); const totalAssetValue = spotAssetValue.add(perpPnl); if (totalLiabilityValue.eq(numericConstants_1.ZERO)) { return numericConstants_1.BN_MAX; } const netAssetValue = totalAssetValue.sub(spotLiabilityValue); return netAssetValue.mul(numericConstants_1.TEN_THOUSAND).div(totalLiabilityValue); } canBeLiquidated() { const liquidationBuffer = this.getLiquidationBuffer(); const totalCollateral = this.getTotalCollateral('Maintenance', undefined, undefined, liquidationBuffer); const marginRequirement = this.getMaintenanceMarginRequirement(liquidationBuff