UNPKG

@drift-labs/sdk

Version:
919 lines (918 loc) • 125 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 spotBalance_2 = require("./math/spotBalance"); const amm_1 = require("./math/amm"); 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; 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 { this.accountSubscriber = new webSocketUserAccountSubscriber_1.WebSocketUserAccountSubscriber(config.driftClient.program, config.userAccountPublicKey, { resubTimeoutMs: (_f = config.accountSubscription) === null || _f === void 0 ? void 0 : _f.resubTimeoutMs, logResubMessages: (_g = config.accountSubscription) === null || _g === void 0 ? void 0 : _g.logResubMessages, }, (_h = config.accountSubscription) === null || _h === void 0 ? void 0 : _h.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); } 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, }; } 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 [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 */ getLPBidAsks(marketIndex, lpShares) { const position = this.getPerpPosition(marketIndex); const lpSharesToCalc = lpShares !== null && lpShares !== void 0 ? lpShares : position === null || position === void 0 ? void 0 : position.lpShares; if (!lpSharesToCalc || lpSharesToCalc.eq(numericConstants_1.ZERO)) { return [numericConstants_1.ZERO, numericConstants_1.ZERO]; } const market = this.driftClient.getPerpMarketAccount(marketIndex); const [marketOpenBids, marketOpenAsks] = (0, amm_1.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 */ getPerpPositionWithLPSettle(marketIndex, originalPosition, burnLpShares = false, includeRemainderInBaseAmount = false) { var _a; originalPosition = (_a = originalPosition !== null && originalPosition !== void 0 ? originalPosition : this.getPerpPosition(marketIndex)) !== null && _a !== void 0 ? _a : this.getEmptyPosition(marketIndex); if (originalPosition.lpShares.eq(numericConstants_1.ZERO)) { return [originalPosition, numericConstants_1.ZERO, numericConstants_1.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 anchor_1.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 = (0, position_1.calculateUnsettledFundingPnl)(market, position); let baseUnit = numericConstants_1.AMM_RESERVE_PRECISION; if (market.amm.perLpBase == position.perLpBase) { if (position.perLpBase >= 0 && position.perLpBase <= numericConstants_1.AMM_RESERVE_PRECISION_EXP.toNumber()) { const marketPerLpRebase = new anchor_1.BN(10 ** market.amm.perLpBase); baseUnit = baseUnit.mul(marketPerLpRebase); } else if (position.perLpBase < 0 && position.perLpBase >= -numericConstants_1.AMM_RESERVE_PRECISION_EXP.toNumber()) { const marketPerLpRebase = new anchor_1.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) { return v.isNeg() ? new anchor_1.BN(-1) : new anchor_1.BN(1); } function standardize(amount, stepSize) { 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 anchor_1.BN(position.remainderBaseAssetAmount), market.amm.orderStepSize); position.baseAssetAmount = position.baseAssetAmount.add(newStandardizedBaa); position.remainderBaseAssetAmount = newRemainderBaa.toNumber(); } let dustBaseAssetValue = numericConstants_1.ZERO; if (burnLpShares && position.remainderBaseAssetAmount != 0) { const oraclePriceData = this.driftClient.getOracleDataForPerpMarket(position.marketIndex); dustBaseAssetValue = new anchor_1.BN(Math.abs(position.remainderBaseAssetAmount)) .mul(oraclePriceData.price) .div(numericConstants_1.AMM_RESERVE_PRECISION) .add(numericConstants_1.ONE); } let updateType; if (position.baseAssetAmount.eq(numericConstants_1.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 = numericConstants_1.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] = (0, amm_1.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(numericConstants_1.ZERO)) { position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateLong; } else if (position.baseAssetAmount.lt(numericConstants_1.ZERO)) { position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateShort; } else { position.lastCumulativeFundingRate = numericConstants_1.ZERO; } const remainderBeforeRemoval = new anchor_1.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 */ getPerpBuyingPower(marketIndex, collateralBuffer = numericConstants_1.ZERO, enterHighLeverageMode = undefined) { const perpPosition = this.getPerpPositionWithLPSettle(marketIndex, undefined, true)[0]; 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); } getPerpBuyingPowerFromFreeCollateralAndBaseAssetAmount(marketIndex, freeCollateral, baseAssetAmount, enterHighLeverageMode = undefined) { const marginRatio = (0, market_1.calculateMarketMarginRatio)(this.driftClient.getPerpMarketAccount(marketIndex), baseAssetAmount, 'Initial', this.getUserAccount().maxMarginRatio, 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) || !pos.lpShares.eq(numericConstants_1.ZERO)); } 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.getOracleDataForPerpMarket(market.marketIndex); const quoteSpotMarket = this.driftClient.getSpotMarketAccount(market.quoteSpotMarketIndex); const quoteOraclePriceData = this.getOracleDataForSpotMarket(market.quoteSpotMarketIndex); if (perpPosition.lpShares.gt(numericConstants_1.ZERO)) { perpPosition = this.getPerpPositionWithLPSettle(perpPosition.marketIndex, undefined, !!withWeightMarginCategory)[0]; } 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.getOracleDataForPerpMarket(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); if (perpPosition.lpShares.gt(numericConstants_1.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 ((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)); if (perpPosition.lpShares.gt(numericConstants_1.ZERO)) { liabilityValue = liabilityValue.add(anchor_1.BN.max(numericConstants_1.QUOTE_PRECISION, valuationPrice .mul(market.amm.orderStepSize) .mul(numericConstants_1.QUOTE_PRECISION) .div(numericConstants_1.AMM_RESERVE_PRECISION) .div(numericConstants_1.PRICE_PRECISION))); } } } 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.getPerpPositionWithLPSettle(marketIndex, undefined, false, true)[0] || this.getEmptyPosition(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.getPerpPositionWithLPSettle(marketIndex, undefined, false, true)[0] || this.getEmptyPosition(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.getOracleDataForPerpMarket(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);