UNPKG

@drift-labs/sdk

Version:
256 lines (255 loc) • 13.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.calculateFundingPool = exports.calculateLongShortFundingRateAndLiveTwaps = exports.calculateLongShortFundingRate = exports.calculateFormattedLiveFundingRate = exports.calculateAllEstimatedFundingRate = void 0; const anchor_1 = require("@coral-xyz/anchor"); const numericConstants_1 = require("../constants/numericConstants"); const bigNum_1 = require("../factory/bigNum"); const types_1 = require("../types"); const amm_1 = require("./amm"); const oracles_1 = require("./oracles"); const utils_1 = require("./utils"); const numericConstants_2 = require("../constants/numericConstants"); function calculateLiveMarkTwap(market, mmOraclePriceData, markPrice, now, period = new anchor_1.BN(3600)) { now = now || new anchor_1.BN((Date.now() / 1000).toFixed(0)); const lastMarkTwapWithMantissa = market.amm.lastMarkPriceTwap; const lastMarkPriceTwapTs = market.amm.lastMarkPriceTwapTs; const timeSinceLastMarkChange = now.sub(lastMarkPriceTwapTs); const markTwapTimeSinceLastUpdate = anchor_1.BN.max(period, anchor_1.BN.max(numericConstants_1.ZERO, period.sub(timeSinceLastMarkChange))); if (!markPrice) { const [bid, ask] = (0, amm_1.calculateBidAskPrice)(market.amm, mmOraclePriceData); markPrice = bid.add(ask).div(new anchor_1.BN(2)); } const markTwapWithMantissa = markTwapTimeSinceLastUpdate .mul(lastMarkTwapWithMantissa) .add(timeSinceLastMarkChange.mul(markPrice)) .div(timeSinceLastMarkChange.add(markTwapTimeSinceLastUpdate)); return markTwapWithMantissa; } function shrinkStaleTwaps(market, markTwapWithMantissa, oracleTwapWithMantissa, now) { now = now || new anchor_1.BN((Date.now() / 1000).toFixed(0)); let newMarkTwap = markTwapWithMantissa; let newOracleTwap = oracleTwapWithMantissa; if (market.amm.lastMarkPriceTwapTs.gt(market.amm.historicalOracleData.lastOraclePriceTwapTs)) { // shrink oracle based on invalid intervals const oracleInvalidDuration = anchor_1.BN.max(numericConstants_1.ZERO, market.amm.lastMarkPriceTwapTs.sub(market.amm.historicalOracleData.lastOraclePriceTwapTs)); const timeSinceLastOracleTwapUpdate = now.sub(market.amm.historicalOracleData.lastOraclePriceTwapTs); const oracleTwapTimeSinceLastUpdate = anchor_1.BN.max(numericConstants_1.ONE, anchor_1.BN.min(market.amm.fundingPeriod, anchor_1.BN.max(numericConstants_1.ONE, market.amm.fundingPeriod.sub(timeSinceLastOracleTwapUpdate)))); newOracleTwap = oracleTwapTimeSinceLastUpdate .mul(oracleTwapWithMantissa) .add(oracleInvalidDuration.mul(markTwapWithMantissa)) .div(oracleTwapTimeSinceLastUpdate.add(oracleInvalidDuration)); } else if (market.amm.lastMarkPriceTwapTs.lt(market.amm.historicalOracleData.lastOraclePriceTwapTs)) { // shrink mark to oracle twap over tradless intervals const tradelessDuration = anchor_1.BN.max(numericConstants_1.ZERO, market.amm.historicalOracleData.lastOraclePriceTwapTs.sub(market.amm.lastMarkPriceTwapTs)); const timeSinceLastMarkTwapUpdate = now.sub(market.amm.lastMarkPriceTwapTs); const markTwapTimeSinceLastUpdate = anchor_1.BN.max(numericConstants_1.ONE, anchor_1.BN.min(market.amm.fundingPeriod, anchor_1.BN.max(numericConstants_1.ONE, market.amm.fundingPeriod.sub(timeSinceLastMarkTwapUpdate)))); newMarkTwap = markTwapTimeSinceLastUpdate .mul(markTwapWithMantissa) .add(tradelessDuration.mul(oracleTwapWithMantissa)) .div(markTwapTimeSinceLastUpdate.add(tradelessDuration)); } return [newMarkTwap, newOracleTwap]; } /** * * @param market * @param oraclePriceData * @param periodAdjustment * @returns Estimated funding rate. : Precision //TODO-PRECISION */ function calculateAllEstimatedFundingRate(market, mmOraclePriceData, oraclePriceData, markPrice, now) { if ((0, types_1.isVariant)(market.status, 'uninitialized')) { return [numericConstants_1.ZERO, numericConstants_1.ZERO, numericConstants_1.ZERO, numericConstants_1.ZERO, numericConstants_1.ZERO]; } // todo: sufficiently differs from blockchain timestamp? now = now || new anchor_1.BN((Date.now() / 1000).toFixed(0)); // calculate real-time mark and oracle twap const liveMarkTwap = calculateLiveMarkTwap(market, mmOraclePriceData, markPrice, now, market.amm.fundingPeriod); const liveOracleTwap = (0, oracles_1.calculateLiveOracleTwap)(market.amm.historicalOracleData, oraclePriceData, now, market.amm.fundingPeriod); const [markTwap, oracleTwap] = shrinkStaleTwaps(market, liveMarkTwap, liveOracleTwap, now); // if(!markTwap.eq(liveMarkTwap)){ // console.log('shrink mark:', liveMarkTwap.toString(), '->', markTwap.toString()); // } // if(!oracleTwap.eq(liveOracleTwap)){ // console.log('shrink orac:', liveOracleTwap.toString(), '->', oracleTwap.toString()); // } const twapSpread = markTwap.sub(oracleTwap); const twapSpreadWithOffset = twapSpread.add(oracleTwap.abs().div(numericConstants_1.FUNDING_RATE_OFFSET_DENOMINATOR)); const maxSpread = getMaxPriceDivergenceForFundingRate(market, oracleTwap); const clampedSpreadWithOffset = (0, utils_1.clampBN)(twapSpreadWithOffset, maxSpread.mul(new anchor_1.BN(-1)), maxSpread); const twapSpreadPct = clampedSpreadWithOffset .mul(numericConstants_1.PRICE_PRECISION) .mul(new anchor_1.BN(100)) .div(oracleTwap); const secondsInHour = new anchor_1.BN(3600); const hoursInDay = new anchor_1.BN(24); const timeSinceLastUpdate = now.sub(market.amm.lastFundingRateTs); const lowerboundEst = twapSpreadPct .mul(market.amm.fundingPeriod) .mul(anchor_1.BN.min(secondsInHour, timeSinceLastUpdate)) .div(secondsInHour) .div(secondsInHour) .div(hoursInDay); const interpEst = twapSpreadPct.div(hoursInDay); const interpRateQuote = twapSpreadPct .div(hoursInDay) .div(numericConstants_1.PRICE_PRECISION.div(numericConstants_1.QUOTE_PRECISION)); let feePoolSize = calculateFundingPool(market); if (interpRateQuote.lt(new anchor_1.BN(0))) { feePoolSize = feePoolSize.mul(new anchor_1.BN(-1)); } let cappedAltEst; let largerSide; let smallerSide; if (market.amm.baseAssetAmountLong.gt(market.amm.baseAssetAmountShort.abs())) { largerSide = market.amm.baseAssetAmountLong.abs(); smallerSide = market.amm.baseAssetAmountShort.abs(); if (twapSpread.gt(new anchor_1.BN(0))) { return [markTwap, oracleTwap, lowerboundEst, interpEst, interpEst]; } } else if (market.amm.baseAssetAmountLong.lt(market.amm.baseAssetAmountShort.abs())) { largerSide = market.amm.baseAssetAmountShort.abs(); smallerSide = market.amm.baseAssetAmountLong.abs(); if (twapSpread.lt(new anchor_1.BN(0))) { return [markTwap, oracleTwap, lowerboundEst, interpEst, interpEst]; } } else { return [markTwap, oracleTwap, lowerboundEst, interpEst, interpEst]; } if (largerSide.gt(numericConstants_1.ZERO)) { // funding smaller flow cappedAltEst = smallerSide.mul(twapSpread).div(hoursInDay); const feePoolTopOff = feePoolSize .mul(numericConstants_1.PRICE_PRECISION.div(numericConstants_1.QUOTE_PRECISION)) .mul(numericConstants_1.AMM_RESERVE_PRECISION); cappedAltEst = cappedAltEst.add(feePoolTopOff).div(largerSide); cappedAltEst = cappedAltEst .mul(numericConstants_1.PRICE_PRECISION) .mul(new anchor_1.BN(100)) .div(oracleTwap); if (cappedAltEst.abs().gte(interpEst.abs())) { cappedAltEst = interpEst; } } else { cappedAltEst = interpEst; } return [markTwap, oracleTwap, lowerboundEst, cappedAltEst, interpEst]; } exports.calculateAllEstimatedFundingRate = calculateAllEstimatedFundingRate; /** * To get funding rate as a percentage, you need to multiply by the funding rate buffer precision * @param rawFundingRate * @returns */ const getFundingRatePct = (rawFundingRate) => { return bigNum_1.BigNum.from(rawFundingRate.mul(numericConstants_2.FUNDING_RATE_BUFFER_PRECISION), numericConstants_2.FUNDING_RATE_PRECISION_EXP).toNum(); }; /** * Calculate funding rates in human-readable form. Values will have some lost precision and shouldn't be used in strict accounting. * @param period : 'hour' | 'year' :: Use 'hour' for the hourly payment as a percentage, 'year' for the payment as an estimated APR. */ function calculateFormattedLiveFundingRate(market, mmOraclePriceData, oraclePriceData, period) { const nowBN = new anchor_1.BN(Date.now() / 1000); const [_markTwapLive, _oracleTwapLive, longFundingRate, shortFundingRate] = calculateLongShortFundingRateAndLiveTwaps(market, mmOraclePriceData, oraclePriceData, undefined, nowBN); let longFundingRateNum = getFundingRatePct(longFundingRate); let shortFundingRateNum = getFundingRatePct(shortFundingRate); if (period == 'year') { const paymentsPerYear = 24 * 365.25; longFundingRateNum *= paymentsPerYear; shortFundingRateNum *= paymentsPerYear; } const longsArePaying = longFundingRateNum > 0; const shortsArePaying = !(shortFundingRateNum > 0); const longsAreString = longsArePaying ? 'pay' : 'receive'; const shortsAreString = !shortsArePaying ? 'receive' : 'pay'; const absoluteLongFundingRateNum = Math.abs(longFundingRateNum); const absoluteShortFundingRateNum = Math.abs(shortFundingRateNum); const formattedLongRatePct = absoluteLongFundingRateNum.toFixed(period == 'hour' ? 5 : 2); const formattedShortRatePct = absoluteShortFundingRateNum.toFixed(period == 'hour' ? 5 : 2); const fundingRateUnit = period == 'year' ? '% APR' : '%'; const formattedFundingRateSummary = `At this rate, longs would ${longsAreString} ${formattedLongRatePct} ${fundingRateUnit} and shorts would ${shortsAreString} ${formattedShortRatePct} ${fundingRateUnit} at the end of the hour.`; return { longRate: longsArePaying ? -absoluteLongFundingRateNum : absoluteLongFundingRateNum, shortRate: shortsArePaying ? -absoluteShortFundingRateNum : absoluteShortFundingRateNum, fundingRateUnit: fundingRateUnit, formattedFundingRateSummary, }; } exports.calculateFormattedLiveFundingRate = calculateFormattedLiveFundingRate; function getMaxPriceDivergenceForFundingRate(market, oracleTwap) { if ((0, types_1.isVariant)(market.contractTier, 'a')) { return oracleTwap.divn(33); } else if ((0, types_1.isVariant)(market.contractTier, 'b')) { return oracleTwap.divn(33); } else if ((0, types_1.isVariant)(market.contractTier, 'c')) { return oracleTwap.divn(20); } else { return oracleTwap.divn(10); } } /** * * @param market * @param oraclePriceData * @param periodAdjustment * @returns Estimated funding rate. : Precision //TODO-PRECISION */ function calculateLongShortFundingRate(market, mmOraclePriceData, oraclePriceData, markPrice, now) { const [_1, _2, _, cappedAltEst, interpEst] = calculateAllEstimatedFundingRate(market, mmOraclePriceData, oraclePriceData, markPrice, now); if (market.amm.baseAssetAmountLong.gt(market.amm.baseAssetAmountShort)) { return [cappedAltEst, interpEst]; } else if (market.amm.baseAssetAmountLong.lt(market.amm.baseAssetAmountShort)) { return [interpEst, cappedAltEst]; } else { return [interpEst, interpEst]; } } exports.calculateLongShortFundingRate = calculateLongShortFundingRate; /** * * @param market * @param oraclePriceData * @param periodAdjustment * @returns Estimated funding rate. : Precision //TODO-PRECISION */ function calculateLongShortFundingRateAndLiveTwaps(market, mmOraclePriceData, oraclePriceData, markPrice, now) { const [markTwapLive, oracleTwapLive, _2, cappedAltEst, interpEst] = calculateAllEstimatedFundingRate(market, mmOraclePriceData, oraclePriceData, markPrice, now); if (market.amm.baseAssetAmountLong.gt(market.amm.baseAssetAmountShort.abs())) { return [markTwapLive, oracleTwapLive, cappedAltEst, interpEst]; } else if (market.amm.baseAssetAmountLong.lt(market.amm.baseAssetAmountShort.abs())) { return [markTwapLive, oracleTwapLive, interpEst, cappedAltEst]; } else { return [markTwapLive, oracleTwapLive, interpEst, interpEst]; } } exports.calculateLongShortFundingRateAndLiveTwaps = calculateLongShortFundingRateAndLiveTwaps; /** * * @param market * @returns Estimated fee pool size */ function calculateFundingPool(market) { // todo const totalFeeLB = market.amm.totalExchangeFee.div(new anchor_1.BN(2)); const feePool = anchor_1.BN.max(numericConstants_1.ZERO, market.amm.totalFeeMinusDistributions .sub(totalFeeLB) .mul(new anchor_1.BN(1)) .div(new anchor_1.BN(3))); return feePool; } exports.calculateFundingPool = calculateFundingPool;