@drift-labs/sdk
Version:
SDK for Drift Protocol
432 lines (386 loc) • 12 kB
text/typescript
import { BN } from '@coral-xyz/anchor';
import {
AMM_RESERVE_PRECISION,
PRICE_PRECISION,
QUOTE_PRECISION,
ZERO,
ONE,
FUNDING_RATE_OFFSET_DENOMINATOR,
} from '../constants/numericConstants';
import { BigNum } from '../factory/bigNum';
import { PerpMarketAccount, isVariant } from '../types';
import { MMOraclePriceData, OraclePriceData } from '../oracles/types';
import { calculateBidAskPrice } from './amm';
import { calculateLiveOracleTwap } from './oracles';
import { clampBN } from './utils';
import {
FUNDING_RATE_BUFFER_PRECISION,
FUNDING_RATE_PRECISION_EXP,
} from '../constants/numericConstants';
function calculateLiveMarkTwap(
market: PerpMarketAccount,
mmOraclePriceData?: MMOraclePriceData,
markPrice?: BN,
now?: BN,
period = new BN(3600)
): BN {
now = now || new BN((Date.now() / 1000).toFixed(0));
const lastMarkTwapWithMantissa = market.amm.lastMarkPriceTwap;
const lastMarkPriceTwapTs = market.amm.lastMarkPriceTwapTs;
const timeSinceLastMarkChange = now.sub(lastMarkPriceTwapTs);
const markTwapTimeSinceLastUpdate = BN.max(
period,
BN.max(ZERO, period.sub(timeSinceLastMarkChange))
);
if (!markPrice) {
const [bid, ask] = calculateBidAskPrice(market.amm, mmOraclePriceData);
markPrice = bid.add(ask).div(new BN(2));
}
const markTwapWithMantissa = markTwapTimeSinceLastUpdate
.mul(lastMarkTwapWithMantissa)
.add(timeSinceLastMarkChange.mul(markPrice))
.div(timeSinceLastMarkChange.add(markTwapTimeSinceLastUpdate));
return markTwapWithMantissa;
}
function shrinkStaleTwaps(
market: PerpMarketAccount,
markTwapWithMantissa: BN,
oracleTwapWithMantissa: BN,
now?: BN
) {
now = now || new 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 = BN.max(
ZERO,
market.amm.lastMarkPriceTwapTs.sub(
market.amm.historicalOracleData.lastOraclePriceTwapTs
)
);
const timeSinceLastOracleTwapUpdate = now.sub(
market.amm.historicalOracleData.lastOraclePriceTwapTs
);
const oracleTwapTimeSinceLastUpdate = BN.max(
ONE,
BN.min(
market.amm.fundingPeriod,
BN.max(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 = BN.max(
ZERO,
market.amm.historicalOracleData.lastOraclePriceTwapTs.sub(
market.amm.lastMarkPriceTwapTs
)
);
const timeSinceLastMarkTwapUpdate = now.sub(market.amm.lastMarkPriceTwapTs);
const markTwapTimeSinceLastUpdate = BN.max(
ONE,
BN.min(
market.amm.fundingPeriod,
BN.max(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
*/
export function calculateAllEstimatedFundingRate(
market: PerpMarketAccount,
mmOraclePriceData?: MMOraclePriceData,
oraclePriceData?: OraclePriceData,
markPrice?: BN,
now?: BN
): [BN, BN, BN, BN, BN] {
if (isVariant(market.status, 'uninitialized')) {
return [ZERO, ZERO, ZERO, ZERO, ZERO];
}
// todo: sufficiently differs from blockchain timestamp?
now = now || new 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 = 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(FUNDING_RATE_OFFSET_DENOMINATOR)
);
const maxSpread = getMaxPriceDivergenceForFundingRate(market, oracleTwap);
const clampedSpreadWithOffset = clampBN(
twapSpreadWithOffset,
maxSpread.mul(new BN(-1)),
maxSpread
);
const twapSpreadPct = clampedSpreadWithOffset
.mul(PRICE_PRECISION)
.mul(new BN(100))
.div(oracleTwap);
const secondsInHour = new BN(3600);
const hoursInDay = new BN(24);
const timeSinceLastUpdate = now.sub(market.amm.lastFundingRateTs);
const lowerboundEst = twapSpreadPct
.mul(market.amm.fundingPeriod)
.mul(BN.min(secondsInHour, timeSinceLastUpdate))
.div(secondsInHour)
.div(secondsInHour)
.div(hoursInDay);
const interpEst = twapSpreadPct.div(hoursInDay);
const interpRateQuote = twapSpreadPct
.div(hoursInDay)
.div(PRICE_PRECISION.div(QUOTE_PRECISION));
let feePoolSize = calculateFundingPool(market);
if (interpRateQuote.lt(new BN(0))) {
feePoolSize = feePoolSize.mul(new BN(-1));
}
let cappedAltEst: BN;
let largerSide: BN;
let smallerSide: BN;
if (
market.amm.baseAssetAmountLong.gt(market.amm.baseAssetAmountShort.abs())
) {
largerSide = market.amm.baseAssetAmountLong.abs();
smallerSide = market.amm.baseAssetAmountShort.abs();
if (twapSpread.gt(new 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 BN(0))) {
return [markTwap, oracleTwap, lowerboundEst, interpEst, interpEst];
}
} else {
return [markTwap, oracleTwap, lowerboundEst, interpEst, interpEst];
}
if (largerSide.gt(ZERO)) {
// funding smaller flow
cappedAltEst = smallerSide.mul(twapSpread).div(hoursInDay);
const feePoolTopOff = feePoolSize
.mul(PRICE_PRECISION.div(QUOTE_PRECISION))
.mul(AMM_RESERVE_PRECISION);
cappedAltEst = cappedAltEst.add(feePoolTopOff).div(largerSide);
cappedAltEst = cappedAltEst
.mul(PRICE_PRECISION)
.mul(new BN(100))
.div(oracleTwap);
if (cappedAltEst.abs().gte(interpEst.abs())) {
cappedAltEst = interpEst;
}
} else {
cappedAltEst = interpEst;
}
return [markTwap, oracleTwap, lowerboundEst, cappedAltEst, interpEst];
}
/**
* To get funding rate as a percentage, you need to multiply by the funding rate buffer precision
* @param rawFundingRate
* @returns
*/
const getFundingRatePct = (rawFundingRate: BN) => {
return BigNum.from(
rawFundingRate.mul(FUNDING_RATE_BUFFER_PRECISION),
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.
*/
export function calculateFormattedLiveFundingRate(
market: PerpMarketAccount,
mmOraclePriceData: MMOraclePriceData,
oraclePriceData: OraclePriceData,
period: 'hour' | 'year'
): {
longRate: number;
shortRate: number;
fundingRateUnit: string;
formattedFundingRateSummary: string;
} {
const nowBN = new 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,
};
}
function getMaxPriceDivergenceForFundingRate(
market: PerpMarketAccount,
oracleTwap: BN
) {
if (isVariant(market.contractTier, 'a')) {
return oracleTwap.divn(33);
} else if (isVariant(market.contractTier, 'b')) {
return oracleTwap.divn(33);
} else if (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
*/
export function calculateLongShortFundingRate(
market: PerpMarketAccount,
mmOraclePriceData?: MMOraclePriceData,
oraclePriceData?: OraclePriceData,
markPrice?: BN,
now?: BN
): [BN, BN] {
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];
}
}
/**
*
* @param market
* @param oraclePriceData
* @param periodAdjustment
* @returns Estimated funding rate. : Precision //TODO-PRECISION
*/
export function calculateLongShortFundingRateAndLiveTwaps(
market: PerpMarketAccount,
mmOraclePriceData?: MMOraclePriceData,
oraclePriceData?: OraclePriceData,
markPrice?: BN,
now?: BN
): [BN, BN, BN, BN] {
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];
}
}
/**
*
* @param market
* @returns Estimated fee pool size
*/
export function calculateFundingPool(market: PerpMarketAccount): BN {
// todo
const totalFeeLB = market.amm.totalExchangeFee.div(new BN(2));
const feePool = BN.max(
ZERO,
market.amm.totalFeeMinusDistributions
.sub(totalFeeLB)
.mul(new BN(1))
.div(new BN(3))
);
return feePool;
}