@drift-labs/sdk
Version:
SDK for Drift Protocol
893 lines • 115 kB
JavaScript
"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