UNPKG

@drift-labs/sdk

Version:
1,251 lines (1,094 loc) • 32 kB
import { BN } from '@coral-xyz/anchor'; import { AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO, PRICE_PRECISION, PEG_PRECISION, ZERO, BID_ASK_SPREAD_PRECISION, ONE, AMM_TO_QUOTE_PRECISION_RATIO, QUOTE_PRECISION, MARGIN_PRECISION, PRICE_DIV_PEG, PERCENTAGE_PRECISION, DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT, FUNDING_RATE_BUFFER_PRECISION, TWO, } from '../constants/numericConstants'; import { AMM, PositionDirection, SwapDirection, PerpMarketAccount, isVariant, } from '../types'; import { assert } from '../assert/assert'; import { squareRootBN, sigNum, clampBN } from './utils'; import { standardizeBaseAssetAmount } from './orders'; import { MMOraclePriceData, OraclePriceData } from '../oracles/types'; import { calculateRepegCost, calculateAdjustKCost, calculateBudgetedPeg, } from './repeg'; import { calculateLiveOracleStd, getNewOracleConfPct } from './oracles'; export function calculatePegFromTargetPrice( targetPrice: BN, baseAssetReserve: BN, quoteAssetReserve: BN ): BN { return BN.max( targetPrice .mul(baseAssetReserve) .div(quoteAssetReserve) .add(PRICE_DIV_PEG.div(new BN(2))) .div(PRICE_DIV_PEG), ONE ); } export function calculateOptimalPegAndBudget( amm: AMM, mmOraclePriceData: MMOraclePriceData ): [BN, BN, BN, boolean] { const reservePriceBefore = calculatePrice( amm.baseAssetReserve, amm.quoteAssetReserve, amm.pegMultiplier ); const targetPrice = mmOraclePriceData.price; const newPeg = calculatePegFromTargetPrice( targetPrice, amm.baseAssetReserve, amm.quoteAssetReserve ); const prePegCost = calculateRepegCost(amm, newPeg); const totalFeeLB = amm.totalExchangeFee.div(new BN(2)); const budget = BN.max(ZERO, amm.totalFeeMinusDistributions.sub(totalFeeLB)); let checkLowerBound = true; if (budget.lt(prePegCost)) { const halfMaxPriceSpread = new BN(amm.maxSpread) .div(new BN(2)) .mul(targetPrice) .div(BID_ASK_SPREAD_PRECISION); let newTargetPrice: BN; let newOptimalPeg: BN; let newBudget: BN; const targetPriceGap = reservePriceBefore.sub(targetPrice); if (targetPriceGap.abs().gt(halfMaxPriceSpread)) { const markAdj = targetPriceGap.abs().sub(halfMaxPriceSpread); if (targetPriceGap.lt(new BN(0))) { newTargetPrice = reservePriceBefore.add(markAdj); } else { newTargetPrice = reservePriceBefore.sub(markAdj); } newOptimalPeg = calculatePegFromTargetPrice( newTargetPrice, amm.baseAssetReserve, amm.quoteAssetReserve ); newBudget = calculateRepegCost(amm, newOptimalPeg); checkLowerBound = false; return [newTargetPrice, newOptimalPeg, newBudget, false]; } else if ( amm.totalFeeMinusDistributions.lt(amm.totalExchangeFee.div(new BN(2))) ) { checkLowerBound = false; } } return [targetPrice, newPeg, budget, checkLowerBound]; } export function calculateNewAmm( amm: AMM, mmOraclePriceData: MMOraclePriceData ): [BN, BN, BN, BN] { let pKNumer = new BN(1); let pKDenom = new BN(1); const [targetPrice, _newPeg, budget, _checkLowerBound] = calculateOptimalPegAndBudget(amm, mmOraclePriceData); let prePegCost = calculateRepegCost(amm, _newPeg); let newPeg = _newPeg; if (prePegCost.gte(budget) && prePegCost.gt(ZERO)) { [pKNumer, pKDenom] = [new BN(999), new BN(1000)]; const deficitMadeup = calculateAdjustKCost(amm, pKNumer, pKDenom); assert(deficitMadeup.lte(new BN(0))); prePegCost = budget.add(deficitMadeup.abs()); const newAmm = Object.assign({}, amm); newAmm.baseAssetReserve = newAmm.baseAssetReserve.mul(pKNumer).div(pKDenom); newAmm.sqrtK = newAmm.sqrtK.mul(pKNumer).div(pKDenom); const invariant = newAmm.sqrtK.mul(newAmm.sqrtK); newAmm.quoteAssetReserve = invariant.div(newAmm.baseAssetReserve); const directionToClose = amm.baseAssetAmountWithAmm.gt(ZERO) ? PositionDirection.SHORT : PositionDirection.LONG; const [newQuoteAssetReserve, _newBaseAssetReserve] = calculateAmmReservesAfterSwap( newAmm, 'base', amm.baseAssetAmountWithAmm.abs(), getSwapDirection('base', directionToClose) ); newAmm.terminalQuoteAssetReserve = newQuoteAssetReserve; newPeg = calculateBudgetedPeg(newAmm, prePegCost, targetPrice); prePegCost = calculateRepegCost(newAmm, newPeg); } return [prePegCost, pKNumer, pKDenom, newPeg]; } export function calculateUpdatedAMM( amm: AMM, mmOraclePriceData: MMOraclePriceData ): AMM { if (amm.curveUpdateIntensity == 0 || mmOraclePriceData === undefined) { return amm; } const newAmm = Object.assign({}, amm); const [prepegCost, pKNumer, pKDenom, newPeg] = calculateNewAmm( amm, mmOraclePriceData ); newAmm.baseAssetReserve = newAmm.baseAssetReserve.mul(pKNumer).div(pKDenom); newAmm.sqrtK = newAmm.sqrtK.mul(pKNumer).div(pKDenom); const invariant = newAmm.sqrtK.mul(newAmm.sqrtK); newAmm.quoteAssetReserve = invariant.div(newAmm.baseAssetReserve); newAmm.pegMultiplier = newPeg; const directionToClose = amm.baseAssetAmountWithAmm.gt(ZERO) ? PositionDirection.SHORT : PositionDirection.LONG; const [newQuoteAssetReserve, _newBaseAssetReserve] = calculateAmmReservesAfterSwap( newAmm, 'base', amm.baseAssetAmountWithAmm.abs(), getSwapDirection('base', directionToClose) ); newAmm.terminalQuoteAssetReserve = newQuoteAssetReserve; newAmm.totalFeeMinusDistributions = newAmm.totalFeeMinusDistributions.sub(prepegCost); newAmm.netRevenueSinceLastFunding = newAmm.netRevenueSinceLastFunding.sub(prepegCost); return newAmm; } export function calculateUpdatedAMMSpreadReserves( amm: AMM, direction: PositionDirection, mmOraclePriceData: MMOraclePriceData, isPrediction = false, latestSlot?: BN ): { baseAssetReserve: BN; quoteAssetReserve: BN; sqrtK: BN; newPeg: BN } { const newAmm = calculateUpdatedAMM(amm, mmOraclePriceData); const [shortReserves, longReserves] = calculateSpreadReserves( newAmm, mmOraclePriceData, undefined, isPrediction, latestSlot ); const dirReserves = isVariant(direction, 'long') ? longReserves : shortReserves; const result = { baseAssetReserve: dirReserves.baseAssetReserve, quoteAssetReserve: dirReserves.quoteAssetReserve, sqrtK: newAmm.sqrtK, newPeg: newAmm.pegMultiplier, }; return result; } export function calculateBidAskPrice( amm: AMM, mmOraclePriceData: MMOraclePriceData, withUpdate = true, isPrediction = false, latestSlot?: BN ): [BN, BN] { let newAmm: AMM; if (withUpdate) { newAmm = calculateUpdatedAMM(amm, mmOraclePriceData); } else { newAmm = amm; } const [bidReserves, askReserves] = calculateSpreadReserves( newAmm, mmOraclePriceData, undefined, isPrediction, latestSlot ); const askPrice = calculatePrice( askReserves.baseAssetReserve, askReserves.quoteAssetReserve, newAmm.pegMultiplier ); const bidPrice = calculatePrice( bidReserves.baseAssetReserve, bidReserves.quoteAssetReserve, newAmm.pegMultiplier ); return [bidPrice, askPrice]; } /** * Calculates a price given an arbitrary base and quote amount (they must have the same precision) * * @param baseAssetReserves * @param quoteAssetReserves * @param pegMultiplier * @returns price : Precision PRICE_PRECISION */ export function calculatePrice( baseAssetReserves: BN, quoteAssetReserves: BN, pegMultiplier: BN ): BN { if (baseAssetReserves.abs().lte(ZERO)) { return new BN(0); } return quoteAssetReserves .mul(PRICE_PRECISION) .mul(pegMultiplier) .div(PEG_PRECISION) .div(baseAssetReserves); } export type AssetType = 'quote' | 'base'; /** * Calculates what the amm reserves would be after swapping a quote or base asset amount. * * @param amm * @param inputAssetType * @param swapAmount * @param swapDirection * @returns quoteAssetReserve and baseAssetReserve after swap. : Precision AMM_RESERVE_PRECISION */ export function calculateAmmReservesAfterSwap( amm: Pick< AMM, 'pegMultiplier' | 'quoteAssetReserve' | 'sqrtK' | 'baseAssetReserve' >, inputAssetType: AssetType, swapAmount: BN, swapDirection: SwapDirection ): [BN, BN] { assert(swapAmount.gte(ZERO), 'swapAmount must be greater than 0'); let newQuoteAssetReserve; let newBaseAssetReserve; if (inputAssetType === 'quote') { swapAmount = swapAmount .mul(AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO) .div(amm.pegMultiplier); [newQuoteAssetReserve, newBaseAssetReserve] = calculateSwapOutput( amm.quoteAssetReserve, swapAmount, swapDirection, amm.sqrtK.mul(amm.sqrtK) ); } else { [newBaseAssetReserve, newQuoteAssetReserve] = calculateSwapOutput( amm.baseAssetReserve, swapAmount, swapDirection, amm.sqrtK.mul(amm.sqrtK) ); } return [newQuoteAssetReserve, newBaseAssetReserve]; } export function calculateMarketOpenBidAsk( baseAssetReserve: BN, minBaseAssetReserve: BN, maxBaseAssetReserve: BN, stepSize?: BN ): [BN, BN] { // open orders let openAsks; if (minBaseAssetReserve.lt(baseAssetReserve)) { openAsks = baseAssetReserve.sub(minBaseAssetReserve).mul(new BN(-1)); if (stepSize && openAsks.abs().div(TWO).lt(stepSize)) { openAsks = ZERO; } } else { openAsks = ZERO; } let openBids; if (maxBaseAssetReserve.gt(baseAssetReserve)) { openBids = maxBaseAssetReserve.sub(baseAssetReserve); if (stepSize && openBids.div(TWO).lt(stepSize)) { openBids = ZERO; } } else { openBids = ZERO; } return [openBids, openAsks]; } export function calculateInventoryLiquidityRatio( baseAssetAmountWithAmm: BN, baseAssetReserve: BN, minBaseAssetReserve: BN, maxBaseAssetReserve: BN ): BN { // inventory skew const [openBids, openAsks] = calculateMarketOpenBidAsk( baseAssetReserve, minBaseAssetReserve, maxBaseAssetReserve ); const minSideLiquidity = BN.min(openBids.abs(), openAsks.abs()); const inventoryScaleBN = BN.min( baseAssetAmountWithAmm .mul(PERCENTAGE_PRECISION) .div(BN.max(minSideLiquidity, ONE)) .abs(), PERCENTAGE_PRECISION ); return inventoryScaleBN; } export function calculateInventoryScale( baseAssetAmountWithAmm: BN, baseAssetReserve: BN, minBaseAssetReserve: BN, maxBaseAssetReserve: BN, directionalSpread: number, maxSpread: number ): number { if (baseAssetAmountWithAmm.eq(ZERO)) { return 1; } const MAX_BID_ASK_INVENTORY_SKEW_FACTOR = BID_ASK_SPREAD_PRECISION.mul( new BN(10) ); const inventoryScaleBN = calculateInventoryLiquidityRatio( baseAssetAmountWithAmm, baseAssetReserve, minBaseAssetReserve, maxBaseAssetReserve ); const inventoryScaleMaxBN = BN.max( MAX_BID_ASK_INVENTORY_SKEW_FACTOR, new BN(maxSpread) .mul(BID_ASK_SPREAD_PRECISION) .div(new BN(Math.max(directionalSpread, 1))) ); const inventoryScaleCapped = BN.min( inventoryScaleMaxBN, BID_ASK_SPREAD_PRECISION.add( inventoryScaleMaxBN.mul(inventoryScaleBN).div(PERCENTAGE_PRECISION) ) ).toNumber() / BID_ASK_SPREAD_PRECISION.toNumber(); return inventoryScaleCapped; } export function calculateReferencePriceOffset( reservePrice: BN, last24hAvgFundingRate: BN, liquidityFraction: BN, oracleTwapFast: BN, markTwapFast: BN, oracleTwapSlow: BN, markTwapSlow: BN, maxOffsetPct: number ): BN { if (last24hAvgFundingRate.eq(ZERO)) { return ZERO; } const maxOffsetInPrice = new BN(maxOffsetPct) .mul(reservePrice) .div(PERCENTAGE_PRECISION); // Calculate quote denominated market premium const markPremiumMinute = clampBN( markTwapFast.sub(oracleTwapFast), maxOffsetInPrice.mul(new BN(-1)), maxOffsetInPrice ); const markPremiumHour = clampBN( markTwapSlow.sub(oracleTwapSlow), maxOffsetInPrice.mul(new BN(-1)), maxOffsetInPrice ); // Convert last24hAvgFundingRate to quote denominated premium const markPremiumDay = clampBN( last24hAvgFundingRate.div(FUNDING_RATE_BUFFER_PRECISION).mul(new BN(24)), maxOffsetInPrice.mul(new BN(-1)), maxOffsetInPrice ); // Take average clamped premium as the price-based offset const markPremiumAvg = markPremiumMinute .add(markPremiumHour) .add(markPremiumDay) .div(new BN(3)); const markPremiumAvgPct = markPremiumAvg .mul(PRICE_PRECISION) .div(reservePrice); const inventoryPct = clampBN( liquidityFraction.mul(new BN(maxOffsetPct)).div(PERCENTAGE_PRECISION), new BN(maxOffsetPct).mul(new BN(-1)), new BN(maxOffsetPct) ); // Only apply when inventory is consistent with recent and 24h market premium let offsetPct = markPremiumAvgPct.add(inventoryPct); if (!sigNum(inventoryPct).eq(sigNum(markPremiumAvgPct))) { offsetPct = ZERO; } const clampedOffsetPct = clampBN( offsetPct, new BN(-maxOffsetPct), new BN(maxOffsetPct) ); return clampedOffsetPct; } export function calculateEffectiveLeverage( baseSpread: number, quoteAssetReserve: BN, terminalQuoteAssetReserve: BN, pegMultiplier: BN, netBaseAssetAmount: BN, reservePrice: BN, totalFeeMinusDistributions: BN ): number { // vAMM skew const netBaseAssetValue = quoteAssetReserve .sub(terminalQuoteAssetReserve) .mul(pegMultiplier) .div(AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO); const localBaseAssetValue = netBaseAssetAmount .mul(reservePrice) .div(AMM_TO_QUOTE_PRECISION_RATIO.mul(PRICE_PRECISION)); const effectiveGap = Math.max( 0, localBaseAssetValue.sub(netBaseAssetValue).toNumber() ); const effectiveLeverage = effectiveGap / (Math.max(0, totalFeeMinusDistributions.toNumber()) + 1) + 1 / QUOTE_PRECISION.toNumber(); return effectiveLeverage; } export function calculateMaxSpread(marginRatioInitial: number): number { const maxTargetSpread: number = new BN(marginRatioInitial) .mul(BID_ASK_SPREAD_PRECISION.div(MARGIN_PRECISION)) .toNumber(); return maxTargetSpread; } export function calculateVolSpreadBN( lastOracleConfPct: BN, reservePrice: BN, markStd: BN, oracleStd: BN, longIntensity: BN, shortIntensity: BN, volume24H: BN ): [BN, BN] { const marketAvgStdPct = markStd .add(oracleStd) .mul(PERCENTAGE_PRECISION) .div(reservePrice) .div(new BN(4)); const volSpread = BN.max(lastOracleConfPct, marketAvgStdPct.div(new BN(2))); const clampMin = PERCENTAGE_PRECISION.div(new BN(100)); const clampMax = PERCENTAGE_PRECISION; const longVolSpreadFactor = clampBN( longIntensity.mul(PERCENTAGE_PRECISION).div(BN.max(ONE, volume24H)), clampMin, clampMax ); const shortVolSpreadFactor = clampBN( shortIntensity.mul(PERCENTAGE_PRECISION).div(BN.max(ONE, volume24H)), clampMin, clampMax ); // only consider confidence interval at full value when above 25 bps let confComponent = lastOracleConfPct; if (lastOracleConfPct.lte(PRICE_PRECISION.div(new BN(400)))) { confComponent = lastOracleConfPct.div(new BN(20)); } const longVolSpread = BN.max( confComponent, volSpread.mul(longVolSpreadFactor).div(PERCENTAGE_PRECISION) ); const shortVolSpread = BN.max( confComponent, volSpread.mul(shortVolSpreadFactor).div(PERCENTAGE_PRECISION) ); return [longVolSpread, shortVolSpread]; } export function calculateSpreadBN( baseSpread: number, lastOracleReservePriceSpreadPct: BN, lastOracleConfPct: BN, maxSpread: number, quoteAssetReserve: BN, terminalQuoteAssetReserve: BN, pegMultiplier: BN, baseAssetAmountWithAmm: BN, reservePrice: BN, totalFeeMinusDistributions: BN, netRevenueSinceLastFunding: BN, baseAssetReserve: BN, minBaseAssetReserve: BN, maxBaseAssetReserve: BN, markStd: BN, oracleStd: BN, longIntensity: BN, shortIntensity: BN, volume24H: BN, ammInventorySpreadAdjustment: number, returnTerms = false ) { assert(Number.isInteger(baseSpread)); assert(Number.isInteger(maxSpread)); const spreadTerms = { longVolSpread: 0, shortVolSpread: 0, longSpreadwPS: 0, shortSpreadwPS: 0, maxTargetSpread: 0, inventorySpreadScale: 0, longSpreadwInvScale: 0, shortSpreadwInvScale: 0, effectiveLeverage: 0, effectiveLeverageCapped: 0, longSpreadwEL: 0, shortSpreadwEL: 0, revenueRetreatAmount: 0, halfRevenueRetreatAmount: 0, longSpreadwRevRetreat: 0, shortSpreadwRevRetreat: 0, longSpreadwOffsetShrink: 0, shortSpreadwOffsetShrink: 0, totalSpread: 0, longSpread: 0, shortSpread: 0, }; const [longVolSpread, shortVolSpread] = calculateVolSpreadBN( lastOracleConfPct, reservePrice, markStd, oracleStd, longIntensity, shortIntensity, volume24H ); spreadTerms.longVolSpread = longVolSpread.toNumber(); spreadTerms.shortVolSpread = shortVolSpread.toNumber(); let longSpread = Math.max(baseSpread / 2, longVolSpread.toNumber()); let shortSpread = Math.max(baseSpread / 2, shortVolSpread.toNumber()); if (lastOracleReservePriceSpreadPct.gt(ZERO)) { shortSpread = Math.max( shortSpread, lastOracleReservePriceSpreadPct.abs().toNumber() + shortVolSpread.toNumber() ); } else if (lastOracleReservePriceSpreadPct.lt(ZERO)) { longSpread = Math.max( longSpread, lastOracleReservePriceSpreadPct.abs().toNumber() + longVolSpread.toNumber() ); } spreadTerms.longSpreadwPS = longSpread; spreadTerms.shortSpreadwPS = shortSpread; const maxSpreadBaseline = Math.min( Math.max( lastOracleReservePriceSpreadPct.abs().toNumber(), lastOracleConfPct.muln(2).toNumber(), BN.max(markStd, oracleStd) .mul(PERCENTAGE_PRECISION) .div(reservePrice) .toNumber() ), BID_ASK_SPREAD_PRECISION.toNumber() ); const maxTargetSpread: number = Math.floor( Math.max(maxSpread, maxSpreadBaseline) ); const inventorySpreadScale = calculateInventoryScale( baseAssetAmountWithAmm, baseAssetReserve, minBaseAssetReserve, maxBaseAssetReserve, baseAssetAmountWithAmm.gt(ZERO) ? longSpread : shortSpread, maxTargetSpread ); if (baseAssetAmountWithAmm.gt(ZERO)) { longSpread *= inventorySpreadScale; } else if (baseAssetAmountWithAmm.lt(ZERO)) { shortSpread *= inventorySpreadScale; } spreadTerms.maxTargetSpread = maxTargetSpread; spreadTerms.inventorySpreadScale = inventorySpreadScale; spreadTerms.longSpreadwInvScale = longSpread; spreadTerms.shortSpreadwInvScale = shortSpread; const MAX_SPREAD_SCALE = 10; if (totalFeeMinusDistributions.gt(ZERO)) { const effectiveLeverage = calculateEffectiveLeverage( baseSpread, quoteAssetReserve, terminalQuoteAssetReserve, pegMultiplier, baseAssetAmountWithAmm, reservePrice, totalFeeMinusDistributions ); spreadTerms.effectiveLeverage = effectiveLeverage; const spreadScale = Math.min(MAX_SPREAD_SCALE, 1 + effectiveLeverage); spreadTerms.effectiveLeverageCapped = spreadScale; if (baseAssetAmountWithAmm.gt(ZERO)) { longSpread *= spreadScale; longSpread = Math.floor(longSpread); } else { shortSpread *= spreadScale; shortSpread = Math.floor(shortSpread); } } else { longSpread *= MAX_SPREAD_SCALE; shortSpread *= MAX_SPREAD_SCALE; } spreadTerms.longSpreadwEL = longSpread; spreadTerms.shortSpreadwEL = shortSpread; if ( netRevenueSinceLastFunding.lt( DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT ) ) { const maxRetreat = maxTargetSpread / 10; let revenueRetreatAmount = maxRetreat; if ( netRevenueSinceLastFunding.gte( DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT.mul(new BN(1000)) ) ) { revenueRetreatAmount = Math.min( maxRetreat, Math.floor( (baseSpread * netRevenueSinceLastFunding.abs().toNumber()) / DEFAULT_REVENUE_SINCE_LAST_FUNDING_SPREAD_RETREAT.abs().toNumber() ) ); } const halfRevenueRetreatAmount = Math.floor(revenueRetreatAmount / 2); spreadTerms.revenueRetreatAmount = revenueRetreatAmount; spreadTerms.halfRevenueRetreatAmount = halfRevenueRetreatAmount; if (baseAssetAmountWithAmm.gt(ZERO)) { longSpread += revenueRetreatAmount; shortSpread += halfRevenueRetreatAmount; } else if (baseAssetAmountWithAmm.lt(ZERO)) { longSpread += halfRevenueRetreatAmount; shortSpread += revenueRetreatAmount; } else { longSpread += halfRevenueRetreatAmount; shortSpread += halfRevenueRetreatAmount; } } spreadTerms.longSpreadwRevRetreat = longSpread; spreadTerms.shortSpreadwRevRetreat = shortSpread; if (ammInventorySpreadAdjustment < 0) { const adjustment = Math.abs(ammInventorySpreadAdjustment); const shrunkLong = Math.max( 1, longSpread - Math.floor((longSpread * adjustment) / 100) ); const shrunkShort = Math.max( 1, shortSpread - Math.floor((shortSpread * adjustment) / 100) ); longSpread = Math.max(longVolSpread.toNumber(), shrunkLong); shortSpread = Math.max(shortVolSpread.toNumber(), shrunkShort); } else if (ammInventorySpreadAdjustment > 0) { const adjustment = ammInventorySpreadAdjustment; const grownLong = Math.max( 1, longSpread + Math.ceil((longSpread * adjustment) / 100) ); const grownShort = Math.max( 1, shortSpread + Math.ceil((shortSpread * adjustment) / 100) ); longSpread = Math.max(longVolSpread.toNumber(), grownLong); shortSpread = Math.max(shortVolSpread.toNumber(), grownShort); } const totalSpread = longSpread + shortSpread; if (totalSpread > maxTargetSpread) { if (longSpread > shortSpread) { longSpread = Math.ceil((longSpread * maxTargetSpread) / totalSpread); shortSpread = Math.floor(maxTargetSpread - longSpread); } else { shortSpread = Math.ceil((shortSpread * maxTargetSpread) / totalSpread); longSpread = Math.floor(maxTargetSpread - shortSpread); } } spreadTerms.totalSpread = totalSpread; spreadTerms.longSpread = longSpread; spreadTerms.shortSpread = shortSpread; if (returnTerms) { return spreadTerms; } return [longSpread, shortSpread]; } export function calculateSpread( amm: AMM, oraclePriceData: OraclePriceData, now?: BN, reservePrice?: BN ): [number, number] { if (amm.baseSpread == 0 || amm.curveUpdateIntensity == 0) { return [amm.baseSpread / 2, amm.baseSpread / 2]; } if (!reservePrice) { reservePrice = calculatePrice( amm.baseAssetReserve, amm.quoteAssetReserve, amm.pegMultiplier ); } const targetPrice = oraclePriceData?.price || reservePrice; const targetMarkSpreadPct = reservePrice .sub(targetPrice) .mul(BID_ASK_SPREAD_PRECISION) .div(reservePrice); now = now || new BN(new Date().getTime() / 1000); //todo const liveOracleStd = calculateLiveOracleStd(amm, oraclePriceData, now); const confIntervalPct = getNewOracleConfPct( amm, oraclePriceData, reservePrice, now ); const spreads = calculateSpreadBN( amm.baseSpread, targetMarkSpreadPct, confIntervalPct, amm.maxSpread, amm.quoteAssetReserve, amm.terminalQuoteAssetReserve, amm.pegMultiplier, amm.baseAssetAmountWithAmm, reservePrice, amm.totalFeeMinusDistributions, amm.netRevenueSinceLastFunding, amm.baseAssetReserve, amm.minBaseAssetReserve, amm.maxBaseAssetReserve, amm.markStd, liveOracleStd, amm.longIntensityVolume, amm.shortIntensityVolume, amm.volume24H, amm.ammInventorySpreadAdjustment ); let longSpread = spreads[0]; let shortSpread = spreads[1]; if (amm.ammSpreadAdjustment > 0) { longSpread = Math.max( longSpread + (longSpread * amm.ammSpreadAdjustment) / 100, 1 ); shortSpread = Math.max( shortSpread + (shortSpread * amm.ammSpreadAdjustment) / 100, 1 ); } else if (amm.ammSpreadAdjustment < 0) { longSpread = Math.max( longSpread - (longSpread * -amm.ammSpreadAdjustment) / 100, 1 ); shortSpread = Math.max( shortSpread - (shortSpread * -amm.ammSpreadAdjustment) / 100, 1 ); } return [longSpread, shortSpread]; } export function getQuoteAssetReservePredictionMarketBounds( amm: AMM, direction: PositionDirection ): [BN, BN] { let quoteAssetReserveLowerBound = ZERO; const pegSqrt = squareRootBN( amm.pegMultiplier.mul(PEG_PRECISION).addn(1) ).addn(1); let quoteAssetReserveUpperBound = amm.sqrtK .mul(pegSqrt) .div(amm.pegMultiplier); if (direction === PositionDirection.LONG) { quoteAssetReserveLowerBound = amm.sqrtK .muln(22361) .mul(pegSqrt) .divn(100000) .div(amm.pegMultiplier); } else { quoteAssetReserveUpperBound = amm.sqrtK .muln(97467) .mul(pegSqrt) .divn(100000) .div(amm.pegMultiplier); } return [quoteAssetReserveLowerBound, quoteAssetReserveUpperBound]; } export function calculateSpreadReserves( amm: AMM, mmOraclePriceData: MMOraclePriceData, now?: BN, isPrediction = false, latestSlot?: BN ) { function calculateSpreadReserve( spread: number, direction: PositionDirection, amm: AMM ): { baseAssetReserve; quoteAssetReserve; } { if (spread === 0) { return { baseAssetReserve: amm.baseAssetReserve, quoteAssetReserve: amm.quoteAssetReserve, }; } let spreadFraction = new BN(spread).div(new BN(2)); // make non-zero if (spreadFraction.eq(ZERO)) { spreadFraction = spread >= 0 ? new BN(1) : new BN(-1); } const quoteAssetReserveDelta = amm.quoteAssetReserve.div( BID_ASK_SPREAD_PRECISION.div(spreadFraction) ); let quoteAssetReserve; if (quoteAssetReserveDelta.gte(ZERO)) { quoteAssetReserve = amm.quoteAssetReserve.add( quoteAssetReserveDelta.abs() ); } else { quoteAssetReserve = amm.quoteAssetReserve.sub( quoteAssetReserveDelta.abs() ); } if (isPrediction) { const [qarLower, qarUpper] = getQuoteAssetReservePredictionMarketBounds( amm, direction ); quoteAssetReserve = clampBN(quoteAssetReserve, qarLower, qarUpper); } const baseAssetReserve = amm.sqrtK.mul(amm.sqrtK).div(quoteAssetReserve); return { baseAssetReserve, quoteAssetReserve, }; } const reservePrice = calculatePrice( amm.baseAssetReserve, amm.quoteAssetReserve, amm.pegMultiplier ); // always allow 10 bps of price offset, up to a half of the market's max_spread let maxOffset = 0; let referencePriceOffset = 0; if (amm.curveUpdateIntensity > 100) { maxOffset = Math.max( amm.maxSpread / 2, (PERCENTAGE_PRECISION.toNumber() / 10000) * (amm.curveUpdateIntensity - 100) ); const liquidityFraction = calculateInventoryLiquidityRatio( amm.baseAssetAmountWithAmm, amm.baseAssetReserve, amm.minBaseAssetReserve, amm.maxBaseAssetReserve ); const liquidityFractionSigned = liquidityFraction.mul( sigNum(amm.baseAssetAmountWithAmm.add(amm.baseAssetAmountWithUnsettledLp)) ); referencePriceOffset = calculateReferencePriceOffset( reservePrice, amm.last24HAvgFundingRate, liquidityFractionSigned, amm.historicalOracleData.lastOraclePriceTwap5Min, amm.lastMarkPriceTwap5Min, amm.historicalOracleData.lastOraclePriceTwap, amm.lastMarkPriceTwap, maxOffset ).toNumber(); } let [longSpread, shortSpread] = calculateSpread( amm, mmOraclePriceData, now, reservePrice ); const doReferencePricOffsetSmooth = Math.sign(referencePriceOffset) !== Math.sign(amm.referencePriceOffset) && amm.curveUpdateIntensity > 100; if (doReferencePricOffsetSmooth) { const slotsPassed = latestSlot != null ? BN.max(latestSlot.sub(amm.lastUpdateSlot), ZERO).toNumber() : 0; const fullOffsetDelta = referencePriceOffset - amm.referencePriceOffset; const raw = Math.trunc( Math.min(Math.abs(fullOffsetDelta), slotsPassed * 1000) / 10 ); const maxAllowed = Math.abs(amm.referencePriceOffset) || Math.abs(referencePriceOffset); const magnitude = Math.min(Math.max(raw, 10), maxAllowed); const referencePriceDelta = Math.sign(fullOffsetDelta) * magnitude; referencePriceOffset = amm.referencePriceOffset + referencePriceDelta; if (referencePriceDelta < 0) { longSpread += Math.abs(referencePriceDelta); shortSpread += Math.abs(referencePriceOffset); } else { shortSpread += Math.abs(referencePriceDelta); longSpread += Math.abs(referencePriceOffset); } } const askReserves = calculateSpreadReserve( longSpread + referencePriceOffset, PositionDirection.LONG, amm ); const bidReserves = calculateSpreadReserve( -shortSpread + referencePriceOffset, PositionDirection.SHORT, amm ); return [bidReserves, askReserves]; } /** * Helper function calculating constant product curve output. Agnostic to whether input asset is quote or base * * @param inputAssetReserve * @param swapAmount * @param swapDirection * @param invariant * @returns newInputAssetReserve and newOutputAssetReserve after swap. : Precision AMM_RESERVE_PRECISION */ export function calculateSwapOutput( inputAssetReserve: BN, swapAmount: BN, swapDirection: SwapDirection, invariant: BN ): [BN, BN] { let newInputAssetReserve; if (swapDirection === SwapDirection.ADD) { newInputAssetReserve = inputAssetReserve.add(swapAmount); } else { newInputAssetReserve = inputAssetReserve.sub(swapAmount); } const newOutputAssetReserve = invariant.div(newInputAssetReserve); return [newInputAssetReserve, newOutputAssetReserve]; } /** * Translate long/shorting quote/base asset into amm operation * * @param inputAssetType * @param positionDirection */ export function getSwapDirection( inputAssetType: AssetType, positionDirection: PositionDirection ): SwapDirection { if (isVariant(positionDirection, 'long') && inputAssetType === 'base') { return SwapDirection.REMOVE; } if (isVariant(positionDirection, 'short') && inputAssetType === 'quote') { return SwapDirection.REMOVE; } return SwapDirection.ADD; } /** * Helper function calculating terminal price of amm * * @param market * @returns cost : Precision PRICE_PRECISION */ export function calculateTerminalPrice(market: PerpMarketAccount) { const directionToClose = market.amm.baseAssetAmountWithAmm.gt(ZERO) ? PositionDirection.SHORT : PositionDirection.LONG; const [newQuoteAssetReserve, newBaseAssetReserve] = calculateAmmReservesAfterSwap( market.amm, 'base', market.amm.baseAssetAmountWithAmm.abs(), getSwapDirection('base', directionToClose) ); const terminalPrice = newQuoteAssetReserve .mul(PRICE_PRECISION) .mul(market.amm.pegMultiplier) .div(PEG_PRECISION) .div(newBaseAssetReserve); return terminalPrice; } export function calculateMaxBaseAssetAmountToTrade( amm: AMM, limit_price: BN, direction: PositionDirection, mmOraclePriceData?: MMOraclePriceData, now?: BN, isPrediction = false ): [BN, PositionDirection] { const invariant = amm.sqrtK.mul(amm.sqrtK); const newBaseAssetReserveSquared = invariant .mul(PRICE_PRECISION) .mul(amm.pegMultiplier) .div(limit_price) .div(PEG_PRECISION); const newBaseAssetReserve = squareRootBN(newBaseAssetReserveSquared); const [shortSpreadReserves, longSpreadReserves] = calculateSpreadReserves( amm, mmOraclePriceData, now, isPrediction ); const baseAssetReserveBefore: BN = isVariant(direction, 'long') ? longSpreadReserves.baseAssetReserve : shortSpreadReserves.baseAssetReserve; if (newBaseAssetReserve.gt(baseAssetReserveBefore)) { return [ newBaseAssetReserve.sub(baseAssetReserveBefore), PositionDirection.SHORT, ]; } else if (newBaseAssetReserve.lt(baseAssetReserveBefore)) { return [ baseAssetReserveBefore.sub(newBaseAssetReserve), PositionDirection.LONG, ]; } else { console.log('tradeSize Too Small'); return [new BN(0), PositionDirection.LONG]; } } export function calculateQuoteAssetAmountSwapped( quoteAssetReserves: BN, pegMultiplier: BN, swapDirection: SwapDirection ): BN { if (isVariant(swapDirection, 'remove')) { quoteAssetReserves = quoteAssetReserves.add(ONE); } let quoteAssetAmount = quoteAssetReserves .mul(pegMultiplier) .div(AMM_TIMES_PEG_TO_QUOTE_PRECISION_RATIO); if (isVariant(swapDirection, 'remove')) { quoteAssetAmount = quoteAssetAmount.add(ONE); } return quoteAssetAmount; } export function calculateMaxBaseAssetAmountFillable( amm: AMM, orderDirection: PositionDirection ): BN { const maxFillSize = amm.baseAssetReserve.div( new BN(amm.maxFillReserveFraction) ); let maxBaseAssetAmountOnSide: BN; if (isVariant(orderDirection, 'long')) { maxBaseAssetAmountOnSide = BN.max( ZERO, amm.baseAssetReserve.sub(amm.minBaseAssetReserve) ); } else { maxBaseAssetAmountOnSide = BN.max( ZERO, amm.maxBaseAssetReserve.sub(amm.baseAssetReserve) ); } return standardizeBaseAssetAmount( BN.min(maxFillSize, maxBaseAssetAmountOnSide), amm.orderStepSize ); }