UNPKG

@quartz-labs/sdk

Version:

SDK for interacting with the Quartz Protocol

655 lines 35.5 kB
import { AMM_RESERVE_PRECISION, AMM_RESERVE_PRECISION_EXP, BN, calculateAssetWeight, calculateLiabilityWeight, calculateLiveOracleTwap, calculateMarketMarginRatio, calculateMarketOpenBidAsk, calculatePerpLiabilityValue, calculatePositionPNL, calculateUnrealizedAssetWeight, calculateUnsettledFundingPnl, calculateWithdrawLimit, calculateWorstCasePerpLiabilityValue, FIVE_MINUTE, getSignedTokenAmount, getStrictTokenValue, getTokenAmount, isSpotPositionAvailable, isVariant, MARGIN_PRECISION, ONE, OPEN_ORDER_MARGIN_REQUIREMENT, PRICE_PRECISION, QUOTE_PRECISION, QUOTE_SPOT_MARKET_INDEX, SPOT_MARKET_WEIGHT_PRECISION, SpotBalanceType, StrictOraclePrice, UserStatus, ZERO, TEN, divCeil, calculateWeightedTokenValue, simulateOrderFill, } from "@drift-labs/sdk"; import { getDriftUserPublicKey, getDriftUserStatsPublicKey, } from "../../utils/accounts.js"; import { getMarketIndicesRecord } from "../../index.browser.js"; export class DriftUser { authority; driftClient; userAccount; pubkey; statsPubkey; constructor(authority, driftClient, userAccount) { this.authority = authority; this.driftClient = driftClient; this.pubkey = getDriftUserPublicKey(this.authority); this.statsPubkey = getDriftUserStatsPublicKey(this.authority); this.userAccount = userAccount; driftClient.addUser(0, this.authority, userAccount); } getDriftUserAccount() { if (!this.userAccount) throw new Error("DriftUser not initialized"); return this.userAccount; } getRemainingAccounts(writeableIndices) { const remainingAccounts = this.driftClient.getRemainingAccounts({ userAccounts: [this.getDriftUserAccount()], useMarketLastSlotCache: true, writableSpotMarketIndexes: writeableIndices, readableSpotMarketIndexes: [QUOTE_SPOT_MARKET_INDEX], }); for (const index of writeableIndices) { const spotMarketAccount = this.driftClient.getSpotMarketAccount(index); if (!spotMarketAccount) throw new Error("Spot market not found"); this.driftClient.addTokenMintToRemainingAccounts(spotMarketAccount, remainingAccounts); } return remainingAccounts; } getHealth() { if (this.isBeingLiquidated()) return 0; // Drift health uses Maintenance margin, Quartz health uses Initial margin const totalCollateral = this.getTotalCollateralValue("Initial", true); const maintenanceMarginReq = this.getInitialMarginRequirement(); if (maintenanceMarginReq.eq(ZERO) && totalCollateral.gte(ZERO)) { return 100; } if (totalCollateral.lte(ZERO)) { return 0; } return Math.round(Math.min(100, Math.max(0, (1 - maintenanceMarginReq.toNumber() / totalCollateral.toNumber()) * 100))); } getTokenAmount(marketIndex, openOrderBalances) { if (!this.userAccount) throw new Error("DriftUser not initialized"); if (!openOrderBalances) openOrderBalances = getMarketIndicesRecord(ZERO); const spotPosition = this.userAccount.spotPositions.find((position) => position.marketIndex === marketIndex); if (spotPosition === undefined) { return ZERO; } const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex); if (!spotMarket) throw new Error("Spot market not found"); return getSignedTokenAmount(getTokenAmount(spotPosition.scaledBalance, spotMarket, spotPosition.balanceType), spotPosition.balanceType).sub(openOrderBalances[marketIndex]); } getWithdrawalLimit(marketIndex, reduceOnly, openOrderBalances) { if (!openOrderBalances) openOrderBalances = getMarketIndicesRecord(ZERO); const nowTs = new BN(Math.floor(Date.now() / 1000)); const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex); if (!spotMarket) throw new Error("Spot market not found"); // eslint-disable-next-line prefer-const let { borrowLimit, withdrawLimit } = calculateWithdrawLimit(spotMarket, nowTs); const freeCollateral = this.getFreeCollateral("Initial", undefined, openOrderBalances); const initialMarginRequirement = this.getMarginRequirement("Initial", undefined, true, true, openOrderBalances); const oracleData = this.driftClient.getOracleDataForSpotMarket(marketIndex); const precisionIncrease = TEN.pow(new BN(spotMarket.decimals - 6)); const { canBypass, depositAmount: userDepositAmount } = this.canBypassWithdrawLimits(marketIndex); if (canBypass) { withdrawLimit = BN.max(withdrawLimit, userDepositAmount); } const assetWeight = calculateAssetWeight(userDepositAmount, oracleData.price, spotMarket, "Initial"); let amountWithdrawable; if (assetWeight.eq(ZERO)) { amountWithdrawable = userDepositAmount; } else if (initialMarginRequirement.eq(ZERO)) { amountWithdrawable = userDepositAmount; } else { amountWithdrawable = divCeil(divCeil(freeCollateral.mul(MARGIN_PRECISION), assetWeight).mul(PRICE_PRECISION), oracleData.price).mul(precisionIncrease); } const maxWithdrawValue = BN.min(BN.min(amountWithdrawable, userDepositAmount), withdrawLimit.abs()); if (reduceOnly) return BN.max(maxWithdrawValue, ZERO); const weightedAssetValue = this.getSpotMarketAssetValue("Initial", marketIndex, false, true, undefined, openOrderBalances); const freeCollatAfterWithdraw = userDepositAmount.gt(ZERO) ? freeCollateral.sub(weightedAssetValue) : freeCollateral; const maxLiabilityAllowed = freeCollatAfterWithdraw .mul(MARGIN_PRECISION) .div(new BN(spotMarket.initialLiabilityWeight)) .mul(PRICE_PRECISION) .div(oracleData.price) .mul(precisionIncrease); const maxBorrowValue = BN.min(maxWithdrawValue.add(maxLiabilityAllowed), borrowLimit.abs()); return BN.max(maxBorrowValue, ZERO); } getFreeCollateral(marginCategory = "Initial", strict = false, openOrderBalances) { const totalCollateral = this.getTotalCollateralValue(marginCategory, strict, true, openOrderBalances); const marginRequirement = marginCategory === "Initial" ? this.getMarginRequirement("Initial", undefined, strict, true, openOrderBalances) : this.getInitialMarginRequirement(openOrderBalances); const freeCollateral = totalCollateral.sub(marginRequirement); return freeCollateral.gte(ZERO) ? freeCollateral : ZERO; } canBypassWithdrawLimits(marketIndex) { if (!this.userAccount) throw new Error("DriftUser not initialized"); const spotMarket = this.driftClient.getSpotMarketAccount(marketIndex); if (!spotMarket) throw new Error("Spot market not found"); const maxDepositAmount = spotMarket.withdrawGuardThreshold.div(new BN(10)); const position = this.userAccount.spotPositions.find((position) => position.marketIndex === marketIndex); const netDeposits = this.userAccount.totalDeposits.sub(this.userAccount.totalWithdraws); if (!position) { return { canBypass: false, maxDepositAmount, depositAmount: ZERO, netDeposits, }; } if (isVariant(position.balanceType, "borrow")) { return { canBypass: false, maxDepositAmount, netDeposits, depositAmount: ZERO, }; } const depositAmount = getTokenAmount(position.scaledBalance, spotMarket, SpotBalanceType.DEPOSIT); if (netDeposits.lt(ZERO)) { return { canBypass: false, maxDepositAmount, depositAmount, netDeposits, }; } return { canBypass: depositAmount.lt(maxDepositAmount), maxDepositAmount, netDeposits, depositAmount, }; } isBeingLiquidated() { if (!this.userAccount) throw new Error("DriftUser not initialized"); return ((this.userAccount.status & (UserStatus.BEING_LIQUIDATED | UserStatus.BANKRUPT)) > 0); } getTotalCollateralValue(marginCategory, strict = false, includeOpenOrders = true, openOrderBalances) { if (!this.userAccount) throw new Error("DriftUser not initialized"); return this.getSpotMarketAssetValue(marginCategory, undefined, includeOpenOrders, strict, undefined, openOrderBalances).add(this.getUnrealizedPNL(true, undefined, marginCategory, strict)); } getTotalSpotLiabilityValue(marginCategory, strict = false, includeOpenOrders = true, openOrderBalances) { if (!this.userAccount) throw new Error("DriftUser not initialized"); const { totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(marginCategory, undefined, undefined, includeOpenOrders, strict, undefined, openOrderBalances); return totalLiabilityValue; } getSpotMarketAssetValue(marginCategory, marketIndex, includeOpenOrders, strict = false, now, openOrderBalances) { const { totalAssetValue } = this.getSpotMarketAssetAndLiabilityValue(marginCategory, marketIndex, undefined, includeOpenOrders, strict, now, openOrderBalances); return totalAssetValue; } getSpotMarketAssetAndLiabilityValue(marginCategory, marketIndex, liquidationBuffer, includeOpenOrders, strict = false, now = new BN(new Date().getTime() / 1000), openOrderBalances) { if (!this.userAccount) throw new Error("DriftUser not initialized"); if (!openOrderBalances) openOrderBalances = getMarketIndicesRecord(ZERO); let netQuoteValue = ZERO; let totalAssetValue = ZERO; let totalLiabilityValue = ZERO; for (const spotPosition of this.userAccount.spotPositions) { const countForBase = marketIndex === undefined || spotPosition.marketIndex === marketIndex; const countForQuote = marketIndex === undefined || marketIndex === QUOTE_SPOT_MARKET_INDEX || (includeOpenOrders && spotPosition.openOrders !== 0); if (isSpotPositionAvailable(spotPosition) || (!countForBase && !countForQuote)) { continue; } const spotMarketAccount = this.driftClient.getSpotMarketAccount(spotPosition.marketIndex); if (!spotMarketAccount) throw new Error("Spot market not found"); const oraclePriceData = this.driftClient.getOracleDataForSpotMarket(spotPosition.marketIndex); let twap5min; if (strict) { twap5min = calculateLiveOracleTwap(spotMarketAccount.historicalOracleData, oraclePriceData, now, FIVE_MINUTE); } const strictOraclePrice = new StrictOraclePrice(oraclePriceData.price, twap5min); const tokenAmount = getSignedTokenAmount(getTokenAmount(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType), spotPosition.balanceType).sub(openOrderBalances[spotPosition.marketIndex]); const isBorrow = tokenAmount.lt(ZERO); if (spotPosition.marketIndex === QUOTE_SPOT_MARKET_INDEX && countForQuote) { if (isBorrow) { 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 (isBorrow) { const liabilityValue = this.getSpotLiabilityValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer).abs(); totalLiabilityValue = totalLiabilityValue.add(liabilityValue); continue; } const assetValue = this.getSpotAssetValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory); totalAssetValue = totalAssetValue.add(assetValue); continue; } const { tokenAmount: worstCaseTokenAmount, ordersValue: worstCaseQuoteTokenAmount, } = this.getWorstCaseTokenAmounts(spotPosition, spotMarketAccount, strictOraclePrice, marginCategory ?? "Initial", this.userAccount.maxMarginRatio, openOrderBalances); if (worstCaseTokenAmount.gt(ZERO) && countForBase) { const baseAssetValue = this.getSpotAssetValue(worstCaseTokenAmount, strictOraclePrice, spotMarketAccount, marginCategory); totalAssetValue = totalAssetValue.add(baseAssetValue); } if (worstCaseTokenAmount.lt(ZERO) && countForBase) { const baseLiabilityValue = this.getSpotLiabilityValue(worstCaseTokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer).abs(); totalLiabilityValue = totalLiabilityValue.add(baseLiabilityValue); } if (worstCaseQuoteTokenAmount.gt(ZERO) && countForQuote) { netQuoteValue = netQuoteValue.add(worstCaseQuoteTokenAmount); } if (worstCaseQuoteTokenAmount.lt(ZERO) && countForQuote) { let weight = SPOT_MARKET_WEIGHT_PRECISION; if (marginCategory === "Initial") { weight = BN.max(weight, new BN(this.userAccount.maxMarginRatio)); } const weightedTokenValue = worstCaseQuoteTokenAmount .abs() .mul(weight) .div(SPOT_MARKET_WEIGHT_PRECISION); netQuoteValue = netQuoteValue.sub(weightedTokenValue); } totalLiabilityValue = totalLiabilityValue.add(new BN(spotPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)); } if (marketIndex === undefined || marketIndex === QUOTE_SPOT_MARKET_INDEX) { if (netQuoteValue.gt(ZERO)) { totalAssetValue = totalAssetValue.add(netQuoteValue); } else { totalLiabilityValue = totalLiabilityValue.add(netQuoteValue.abs()); } } return { totalAssetValue, totalLiabilityValue }; } getWorstCaseTokenAmounts(spotPosition, spotMarketAccount, strictOraclePrice, marginCategory, customMarginRatio, openOrderBalances) { if (!openOrderBalances) openOrderBalances = getMarketIndicesRecord(ZERO); const tokenAmount = getSignedTokenAmount(getTokenAmount(spotPosition.scaledBalance, spotMarketAccount, spotPosition.balanceType), spotPosition.balanceType).sub(openOrderBalances[spotPosition.marketIndex]); const tokenValue = getStrictTokenValue(tokenAmount, spotMarketAccount.decimals, strictOraclePrice); if (spotPosition.openBids.eq(ZERO) && spotPosition.openAsks.eq(ZERO)) { const { weight, weightedTokenValue } = calculateWeightedTokenValue(tokenAmount, tokenValue, strictOraclePrice.current, spotMarketAccount, marginCategory, customMarginRatio); return { tokenAmount, ordersValue: ZERO, tokenValue, weight, weightedTokenValue, freeCollateralContribution: weightedTokenValue, }; } const bidsSimulation = simulateOrderFill(tokenAmount, tokenValue, spotPosition.openBids, strictOraclePrice, spotMarketAccount, marginCategory, customMarginRatio); const asksSimulation = simulateOrderFill(tokenAmount, tokenValue, spotPosition.openAsks, strictOraclePrice, spotMarketAccount, marginCategory, customMarginRatio); if (asksSimulation.freeCollateralContribution.lt(bidsSimulation.freeCollateralContribution)) { return asksSimulation; } return bidsSimulation; } getSpotLiabilityValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory, liquidationBuffer) { if (!this.userAccount) throw new Error("DriftUser not initialized"); let liabilityValue = getStrictTokenValue(tokenAmount, spotMarketAccount.decimals, strictOraclePrice); if (marginCategory !== undefined) { let weight = calculateLiabilityWeight(tokenAmount, spotMarketAccount, marginCategory); if (marginCategory === "Initial" && spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX) { weight = BN.max(weight, SPOT_MARKET_WEIGHT_PRECISION.addn(this.userAccount.maxMarginRatio)); } if (liquidationBuffer !== undefined) { weight = weight.add(liquidationBuffer); } liabilityValue = liabilityValue .mul(weight) .div(SPOT_MARKET_WEIGHT_PRECISION); } return liabilityValue; } getSpotAssetValue(tokenAmount, strictOraclePrice, spotMarketAccount, marginCategory) { if (!this.userAccount) throw new Error("DriftUser not initialized"); let assetValue = getStrictTokenValue(tokenAmount, spotMarketAccount.decimals, strictOraclePrice); if (marginCategory !== undefined) { let weight = calculateAssetWeight(tokenAmount, strictOraclePrice.current, spotMarketAccount, marginCategory); if (marginCategory === "Initial" && spotMarketAccount.marketIndex !== QUOTE_SPOT_MARKET_INDEX) { const userCustomAssetWeight = BN.max(ZERO, SPOT_MARKET_WEIGHT_PRECISION.subn(this.userAccount.maxMarginRatio)); weight = BN.min(weight, userCustomAssetWeight); } assetValue = assetValue.mul(weight).div(SPOT_MARKET_WEIGHT_PRECISION); } return assetValue; } getUnrealizedPNL(withFunding, marketIndex, withWeightMarginCategory, strict = false) { return this.getActivePerpPositions() .filter((pos) => marketIndex !== undefined ? pos.marketIndex === marketIndex : true) .reduce((unrealizedPnl, perpPosition) => { const market = this.driftClient.getPerpMarketAccount(perpPosition.marketIndex); if (!market) throw new Error("Perp market not found"); const oraclePriceData = this.driftClient.getOracleDataForPerpMarket(market.marketIndex); const quoteSpotMarket = this.driftClient.getSpotMarketAccount(market.quoteSpotMarketIndex); if (!quoteSpotMarket) throw new Error("Quote spot market not found"); const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(market.quoteSpotMarketIndex); if (perpPosition.lpShares.gt(ZERO)) { perpPosition = this.getPerpPositionWithLPSettle(perpPosition.marketIndex, undefined, !!withWeightMarginCategory)[0]; } let positionUnrealizedPnl = calculatePositionPNL(market, perpPosition, withFunding, oraclePriceData); let quotePrice; if (strict && positionUnrealizedPnl.gt(ZERO)) { quotePrice = BN.min(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min); } else if (strict && positionUnrealizedPnl.lt(ZERO)) { quotePrice = BN.max(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min); } else { quotePrice = quoteOraclePriceData.price; } positionUnrealizedPnl = positionUnrealizedPnl .mul(quotePrice) .div(PRICE_PRECISION); if (withWeightMarginCategory !== undefined) { if (positionUnrealizedPnl.gt(ZERO)) { positionUnrealizedPnl = positionUnrealizedPnl .mul(calculateUnrealizedAssetWeight(market, quoteSpotMarket, positionUnrealizedPnl, withWeightMarginCategory, oraclePriceData)) .div(new BN(SPOT_MARKET_WEIGHT_PRECISION)); } } return unrealizedPnl.add(positionUnrealizedPnl); }, ZERO); } getActivePerpPositions() { if (!this.userAccount) throw new Error("DriftUser not initialized"); return this.userAccount.perpPositions.filter((pos) => !pos.baseAssetAmount.eq(ZERO) || !pos.quoteAssetAmount.eq(ZERO) || !(pos.openOrders === 0) || !pos.lpShares.eq(ZERO)); } getPerpPositionWithLPSettle(marketIndex, originalPosition, burnLpShares = false, includeRemainderInBaseAmount = false) { originalPosition = originalPosition ?? this.getPerpPosition(marketIndex) ?? this.getEmptyPosition(marketIndex); if (originalPosition.lpShares.eq(ZERO)) { return [originalPosition, ZERO, ZERO]; } const position = this.getClonedPosition(originalPosition); const market = this.driftClient.getPerpMarketAccount(position.marketIndex); if (!market) throw new Error("Perp market not found"); if (market.amm.perLpBase !== position.perLpBase) { // perLpBase = 1 => per 10 LP shares, perLpBase = -1 => per 0.1 LP shares const expoDiff = market.amm.perLpBase - position.perLpBase; const marketPerLpRebaseScalar = new BN(10 ** Math.abs(expoDiff)); if (expoDiff > 0) { position.lastBaseAssetAmountPerLp = position.lastBaseAssetAmountPerLp.mul(marketPerLpRebaseScalar); position.lastQuoteAssetAmountPerLp = position.lastQuoteAssetAmountPerLp.mul(marketPerLpRebaseScalar); } else { position.lastBaseAssetAmountPerLp = position.lastBaseAssetAmountPerLp.div(marketPerLpRebaseScalar); position.lastQuoteAssetAmountPerLp = position.lastQuoteAssetAmountPerLp.div(marketPerLpRebaseScalar); } position.perLpBase = position.perLpBase + expoDiff; } const nShares = position.lpShares; // incorp unsettled funding on pre settled position const quoteFundingPnl = calculateUnsettledFundingPnl(market, position); let baseUnit = AMM_RESERVE_PRECISION; if (market.amm.perLpBase === position.perLpBase) { if (position.perLpBase >= 0 && position.perLpBase <= AMM_RESERVE_PRECISION_EXP.toNumber()) { const marketPerLpRebase = new BN(10 ** market.amm.perLpBase); baseUnit = baseUnit.mul(marketPerLpRebase); } else if (position.perLpBase < 0 && position.perLpBase >= -AMM_RESERVE_PRECISION_EXP.toNumber()) { const marketPerLpRebase = new BN(10 ** Math.abs(market.amm.perLpBase)); baseUnit = baseUnit.div(marketPerLpRebase); } else { throw "cannot calc"; } } else { throw "market.amm.perLpBase != position.perLpBase"; } const deltaBaa = market.amm.baseAssetAmountPerLp .sub(position.lastBaseAssetAmountPerLp) .mul(nShares) .div(baseUnit); const deltaQaa = market.amm.quoteAssetAmountPerLp .sub(position.lastQuoteAssetAmountPerLp) .mul(nShares) .div(baseUnit); function sign(v) { return v.isNeg() ? new BN(-1) : new 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); if (standardizedBaa === undefined) throw new Error("Standardized BAA is undefined"); if (remainderBaa === undefined) throw new Error("Remainder BAA is undefined"); position.remainderBaseAssetAmount += remainderBaa.toNumber(); if (Math.abs(position.remainderBaseAssetAmount) > market.amm.orderStepSize.toNumber()) { const [newStandardizedBaa, newRemainderBaa] = standardize(new BN(position.remainderBaseAssetAmount), market.amm.orderStepSize); if (newStandardizedBaa === undefined) throw new Error("New standardized BAA is undefined"); if (newRemainderBaa === undefined) throw new Error("New remainder BAA is undefined"); position.baseAssetAmount = position.baseAssetAmount.add(newStandardizedBaa); position.remainderBaseAssetAmount = newRemainderBaa.toNumber(); } let dustBaseAssetValue = ZERO; if (burnLpShares && position.remainderBaseAssetAmount !== 0) { const oraclePriceData = this.driftClient.getOracleDataForPerpMarket(position.marketIndex); dustBaseAssetValue = new BN(Math.abs(position.remainderBaseAssetAmount)) .mul(oraclePriceData.price) .div(AMM_RESERVE_PRECISION) .add(ONE); } let updateType; if (position.baseAssetAmount.eq(ZERO)) { updateType = "open"; } else if (sign(position.baseAssetAmount).eq(sign(deltaBaa))) { updateType = "increase"; } else if (position.baseAssetAmount.abs().gt(deltaBaa.abs())) { updateType = "reduce"; } else if (position.baseAssetAmount.abs().eq(deltaBaa.abs())) { updateType = "close"; } else { updateType = "flip"; } let newQuoteEntry; let pnl; if (updateType === "open" || updateType === "increase") { newQuoteEntry = position.quoteEntryAmount.add(deltaQaa); pnl = ZERO; } else if (updateType === "reduce" || updateType === "close") { newQuoteEntry = position.quoteEntryAmount.sub(position.quoteEntryAmount .mul(deltaBaa.abs()) .div(position.baseAssetAmount.abs())); pnl = position.quoteEntryAmount.sub(newQuoteEntry).add(deltaQaa); } else { newQuoteEntry = deltaQaa.sub(deltaQaa.mul(position.baseAssetAmount.abs()).div(deltaBaa.abs())); pnl = position.quoteEntryAmount.add(deltaQaa.sub(newQuoteEntry)); } position.quoteEntryAmount = newQuoteEntry; position.baseAssetAmount = position.baseAssetAmount.add(standardizedBaa); position.quoteAssetAmount = position.quoteAssetAmount .add(deltaQaa) .add(quoteFundingPnl) .sub(dustBaseAssetValue); position.quoteBreakEvenAmount = position.quoteBreakEvenAmount .add(deltaQaa) .add(quoteFundingPnl) .sub(dustBaseAssetValue); // update open bids/asks const [marketOpenBids, marketOpenAsks] = calculateMarketOpenBidAsk(market.amm.baseAssetReserve, market.amm.minBaseAssetReserve, market.amm.maxBaseAssetReserve, market.amm.orderStepSize); const lpOpenBids = marketOpenBids .mul(position.lpShares) .div(market.amm.sqrtK); const lpOpenAsks = marketOpenAsks .mul(position.lpShares) .div(market.amm.sqrtK); position.openBids = lpOpenBids.add(position.openBids); position.openAsks = lpOpenAsks.add(position.openAsks); // eliminate counting funding on settled position if (position.baseAssetAmount.gt(ZERO)) { position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateLong; } else if (position.baseAssetAmount.lt(ZERO)) { position.lastCumulativeFundingRate = market.amm.cumulativeFundingRateShort; } else { position.lastCumulativeFundingRate = ZERO; } const remainderBeforeRemoval = new BN(position.remainderBaseAssetAmount); if (includeRemainderInBaseAmount) { position.baseAssetAmount = position.baseAssetAmount.add(remainderBeforeRemoval); position.remainderBaseAssetAmount = 0; } return [position, remainderBeforeRemoval, pnl]; } getPerpPosition(marketIndex) { if (!this.userAccount) throw new Error("DriftUser not initialized"); const activePositions = this.userAccount.perpPositions.filter((pos) => !pos.baseAssetAmount.eq(ZERO) || !pos.quoteAssetAmount.eq(ZERO) || !(pos.openOrders === 0) || !pos.lpShares.eq(ZERO)); return activePositions.find((position) => position.marketIndex === marketIndex); } getEmptyPosition(marketIndex) { return { baseAssetAmount: ZERO, remainderBaseAssetAmount: 0, lastCumulativeFundingRate: ZERO, marketIndex, quoteAssetAmount: ZERO, quoteEntryAmount: ZERO, quoteBreakEvenAmount: ZERO, openOrders: 0, openBids: ZERO, openAsks: ZERO, settledPnl: ZERO, lpShares: ZERO, lastBaseAssetAmountPerLp: ZERO, lastQuoteAssetAmountPerLp: ZERO, perLpBase: 0, maxMarginRatio: 0, isolatedPositionScaledBalance: ZERO, positionFlag: 0, }; } getClonedPosition(position) { const clonedPosition = Object.assign({}, position); return clonedPosition; } getInitialMarginRequirement(openOrderBalances) { if (!this.userAccount) throw new Error("DriftUser not initialized"); // if user being liq'd, can continue to be liq'd until total collateral above the margin requirement plus buffer let liquidationBuffer = undefined; if (this.isBeingLiquidated()) { liquidationBuffer = new BN(this.driftClient.getStateAccount().liquidationMarginBufferRatio); } return this.getMarginRequirement("Initial", liquidationBuffer, false, true, openOrderBalances); } getMarginRequirement(marginCategory, liquidationBuffer, strict = false, includeOpenOrders = true, openOrderBalances) { return this.getTotalPerpPositionLiability(marginCategory, liquidationBuffer, includeOpenOrders, strict).add(this.getSpotMarketLiabilityValue(marginCategory, undefined, liquidationBuffer, includeOpenOrders, strict, undefined, openOrderBalances)); } getTotalPerpPositionLiability(marginCategory, liquidationBuffer, includeOpenOrders, strict = false) { return this.getActivePerpPositions().reduce((totalPerpValue, perpPosition) => { const baseAssetValue = this.calculateWeightedPerpPositionLiability(perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict); return totalPerpValue.add(baseAssetValue); }, ZERO); } calculateWeightedPerpPositionLiability(perpPosition, marginCategory, liquidationBuffer, includeOpenOrders, strict = false) { if (!this.userAccount) throw new Error("DriftUser not initialized"); const market = this.driftClient.getPerpMarketAccount(perpPosition.marketIndex); if (!market) throw new Error("Perp market not found"); if (perpPosition.lpShares.gt(ZERO)) { // is an lp, clone so we dont mutate the position perpPosition = this.getPerpPositionWithLPSettle(market.marketIndex, this.getClonedPosition(perpPosition), !!marginCategory)[0]; } let valuationPrice = this.driftClient.getOracleDataForPerpMarket(market.marketIndex).price; if (isVariant(market.status, "settlement")) { valuationPrice = market.expiryPrice; } let baseAssetAmount; let liabilityValue; if (includeOpenOrders) { const { worstCaseBaseAssetAmount, worstCaseLiabilityValue } = calculateWorstCasePerpLiabilityValue(perpPosition, market, valuationPrice); baseAssetAmount = worstCaseBaseAssetAmount; liabilityValue = worstCaseLiabilityValue; } else { baseAssetAmount = perpPosition.baseAssetAmount; liabilityValue = calculatePerpLiabilityValue(baseAssetAmount, valuationPrice, isVariant(market.contractType, "prediction")); } if (marginCategory) { let marginRatio = new BN(calculateMarketMarginRatio(market, baseAssetAmount.abs(), marginCategory, this.userAccount.maxMarginRatio, isVariant(this.userAccount.marginMode, "highLeverage"))); if (liquidationBuffer !== undefined) { marginRatio = marginRatio.add(liquidationBuffer); } if (isVariant(market.status, "settlement")) { marginRatio = ZERO; } const quoteSpotMarket = this.driftClient.getSpotMarketAccount(market.quoteSpotMarketIndex); if (!quoteSpotMarket) throw new Error("Quote spot market not found"); const quoteOraclePriceData = this.driftClient.getOracleDataForSpotMarket(QUOTE_SPOT_MARKET_INDEX); let quotePrice; if (strict) { quotePrice = BN.max(quoteOraclePriceData.price, quoteSpotMarket.historicalOracleData.lastOraclePriceTwap5Min); } else { quotePrice = quoteOraclePriceData.price; } liabilityValue = liabilityValue .mul(quotePrice) .div(PRICE_PRECISION) .mul(marginRatio) .div(MARGIN_PRECISION); if (includeOpenOrders) { liabilityValue = liabilityValue.add(new BN(perpPosition.openOrders).mul(OPEN_ORDER_MARGIN_REQUIREMENT)); if (perpPosition.lpShares.gt(ZERO)) { liabilityValue = liabilityValue.add(BN.max(QUOTE_PRECISION, valuationPrice .mul(market.amm.orderStepSize) .mul(QUOTE_PRECISION) .div(AMM_RESERVE_PRECISION) .div(PRICE_PRECISION))); } } } return liabilityValue; } getSpotMarketLiabilityValue(marginCategory, marketIndex, liquidationBuffer, includeOpenOrders, strict = false, now, openOrderBalances) { const { totalLiabilityValue } = this.getSpotMarketAssetAndLiabilityValue(marginCategory, marketIndex, liquidationBuffer, includeOpenOrders, strict, now, openOrderBalances); return totalLiabilityValue; } } //# sourceMappingURL=DriftUser.class.js.map