diff --git a/CHANGELOG.md b/CHANGELOG.md index 72879d4fd..6bcc140cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixes -- ts-sd: fix oracle is valid ([#806](https://github.com/drift-labs/protocol-v2/pull/806)) +- ts-sdk: contract tier funding rate clamp ([#785](https://github.com/drift-labs/protocol-v2/pull/785)) +- ts-sdk: fix oracle is valid ([#806](https://github.com/drift-labs/protocol-v2/pull/806)) ### Breaking diff --git a/sdk/src/math/funding.ts b/sdk/src/math/funding.ts index 46e1e3db5..e33693785 100644 --- a/sdk/src/math/funding.ts +++ b/sdk/src/math/funding.ts @@ -7,10 +7,11 @@ import { ONE, FUNDING_RATE_OFFSET_DENOMINATOR, } from '../constants/numericConstants'; -import { PerpMarketAccount, isVariant } from '../types'; +import { ContractTier, PerpMarketAccount, isVariant } from '../types'; import { OraclePriceData } from '../oracles/types'; import { calculateBidAskPrice } from './amm'; import { calculateLiveOracleTwap } from './oracles'; +import { clampBN } from './utils'; function calculateLiveMarkTwap( market: PerpMarketAccount, @@ -160,8 +161,15 @@ export async function calculateAllEstimatedFundingRate( const twapSpreadWithOffset = twapSpread.add( oracleTwap.abs().div(FUNDING_RATE_OFFSET_DENOMINATOR) ); + const maxSpread = getMaxPriceDivergenceForFundingRate(market, oracleTwap); - const twapSpreadPct = twapSpreadWithOffset + const clampedSpreadWithOffset = clampBN( + twapSpreadWithOffset, + maxSpread.mul(new BN(-1)), + maxSpread + ); + + const twapSpreadPct = clampedSpreadWithOffset .mul(PRICE_PRECISION) .mul(new BN(100)) .div(oracleTwap); @@ -234,6 +242,25 @@ export async function calculateAllEstimatedFundingRate( return [markTwap, oracleTwap, lowerboundEst, cappedAltEst, interpEst]; } +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 if (isVariant(market.contractTier, 'speculative')) { + return oracleTwap.divn(10); + } else if (isVariant(market.contractTier, 'isolated')) { + return oracleTwap.divn(10); + } else { + return oracleTwap.divn(10); + } +} + /** * * @param market diff --git a/sdk/tests/amm/test.ts b/sdk/tests/amm/test.ts index 9a54ec143..abc75015f 100644 --- a/sdk/tests/amm/test.ts +++ b/sdk/tests/amm/test.ts @@ -28,6 +28,7 @@ import { squareRootBN, calculateReferencePriceOffset, calculateInventoryLiquidityRatio, + ContractTier, isOracleValid, OracleGuardRails, } from '../../src'; @@ -1189,6 +1190,154 @@ describe('AMM Tests', () => { assert(est2.eq(new BN('-719'))); }); + it('predicted funding rate mock clamp', async () => { + const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets); + const mockMarket1 = myMockPerpMarkets[0]; + + // make it like OP + const now = new BN(1688881915); + + mockMarket1.amm.fundingPeriod = new BN(3600); + mockMarket1.amm.lastFundingRateTs = new BN(1688864415); + + const currentMarkPrice = new BN(1.2242 * PRICE_PRECISION.toNumber()); // trading at a premium + const oraclePriceData: OraclePriceData = { + price: new BN(1.924 * PRICE_PRECISION.toNumber()), + slot: new BN(0), + confidence: new BN(1), + hasSufficientNumberOfDataPoints: true, + }; + mockMarket1.amm.historicalOracleData.lastOraclePrice = new BN( + 1.9535 * PRICE_PRECISION.toNumber() + ); + + // mockMarket1.amm.pegMultiplier = new BN(1.897573 * 1e3); + + mockMarket1.amm.lastMarkPriceTwap = new BN( + 1.218363 * PRICE_PRECISION.toNumber() + ); + mockMarket1.amm.lastBidPriceTwap = new BN( + 1.218363 * PRICE_PRECISION.toNumber() + ); + mockMarket1.amm.lastAskPriceTwap = new BN( + 1.218364 * PRICE_PRECISION.toNumber() + ); + mockMarket1.amm.lastMarkPriceTwapTs = new BN(1688878815); + + mockMarket1.amm.historicalOracleData.lastOraclePriceTwap = new BN( + 1.820964 * PRICE_PRECISION.toNumber() + ); + mockMarket1.amm.historicalOracleData.lastOraclePriceTwapTs = new BN( + 1688879991 + ); + mockMarket1.contractTier = ContractTier.A; + + const [ + _markTwapLive, + _oracleTwapLive, + _lowerboundEst, + _cappedAltEst, + _interpEst, + ] = await calculateAllEstimatedFundingRate( + mockMarket1, + oraclePriceData, + currentMarkPrice, + now + ); + + // console.log(_markTwapLive.toString()); + // console.log(_oracleTwapLive.toString()); + // console.log(_lowerboundEst.toString()); + // console.log(_cappedAltEst.toString()); + // console.log(_interpEst.toString()); + // console.log('-----'); + + let [markTwapLive, oracleTwapLive, est1, est2] = + await calculateLongShortFundingRateAndLiveTwaps( + mockMarket1, + oraclePriceData, + currentMarkPrice, + now + ); + + console.log( + 'markTwapLive:', + mockMarket1.amm.lastMarkPriceTwap.toString(), + '->', + markTwapLive.toString() + ); + console.log( + 'oracTwapLive:', + mockMarket1.amm.historicalOracleData.lastOraclePriceTwap.toString(), + '->', + oracleTwapLive.toString() + ); + console.log('pred funding:', est1.toString(), est2.toString()); + + assert(markTwapLive.eq(new BN('1680634'))); + assert(oracleTwapLive.eq(new BN('1876031'))); + assert(est1.eq(est2)); + assert(est2.eq(new BN('-126261'))); + + mockMarket1.contractTier = ContractTier.C; + + [markTwapLive, oracleTwapLive, est1, est2] = + await calculateLongShortFundingRateAndLiveTwaps( + mockMarket1, + oraclePriceData, + currentMarkPrice, + now + ); + + console.log( + 'markTwapLive:', + mockMarket1.amm.lastMarkPriceTwap.toString(), + '->', + markTwapLive.toString() + ); + console.log( + 'oracTwapLive:', + mockMarket1.amm.historicalOracleData.lastOraclePriceTwap.toString(), + '->', + oracleTwapLive.toString() + ); + console.log('pred funding:', est1.toString(), est2.toString()); + + assert(markTwapLive.eq(new BN('1680634'))); + assert(oracleTwapLive.eq(new BN('1876031'))); + assert(est1.eq(est2)); + assert(est2.eq(new BN('-208332'))); + + mockMarket1.contractTier = ContractTier.SPECULATIVE; + + [markTwapLive, oracleTwapLive, est1, est2] = + await calculateLongShortFundingRateAndLiveTwaps( + mockMarket1, + oraclePriceData, + currentMarkPrice, + now + ); + + console.log( + 'markTwapLive:', + mockMarket1.amm.lastMarkPriceTwap.toString(), + '->', + markTwapLive.toString() + ); + console.log( + 'oracTwapLive:', + mockMarket1.amm.historicalOracleData.lastOraclePriceTwap.toString(), + '->', + oracleTwapLive.toString() + ); + console.log('pred funding:', est1.toString(), est2.toString()); + + assert(markTwapLive.eq(new BN('1680634'))); + assert(oracleTwapLive.eq(new BN('1876031'))); + assert(est1.eq(est2)); + assert(est2.eq(new BN('-416666'))); + }); + it('orderbook L2 gen (no topOfBookQuoteAmounts, 10 numOrders, low liquidity)', async () => { const myMockPerpMarkets = _.cloneDeep(mockPerpMarkets);