@drift-labs/sdk
Version:
SDK for Drift Protocol
256 lines (255 loc) • 13.2 kB
JavaScript
"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;