diff --git a/src/utils/tests/auctionParams.test.ts b/src/utils/tests/auctionParams.test.ts index 5bfad84..f9f4b2b 100644 --- a/src/utils/tests/auctionParams.test.ts +++ b/src/utils/tests/auctionParams.test.ts @@ -727,7 +727,6 @@ describe('calculateDynamicSlippage - crossed book handling', () => { const worstPrice = new BN(100).mul(PRICE_PRECISION); const slip = calculateDynamicSlippage( - 'long', 0, // major perp 'perp', mockDriftClient, @@ -762,7 +761,6 @@ describe('calculateDynamicSlippage - crossed book handling', () => { const worstPrice = new BN(100).mul(PRICE_PRECISION); const slipNormal = calculateDynamicSlippage( - 'long', 0, 'perp', mockDriftClient, @@ -777,7 +775,6 @@ describe('calculateDynamicSlippage - crossed book handling', () => { } as any; const slipCrossed = calculateDynamicSlippage( - 'long', 0, 'perp', mockDriftClient, @@ -790,338 +787,3 @@ describe('calculateDynamicSlippage - crossed book handling', () => { expect(slipNormal).toBeGreaterThanOrEqual(slipCrossed); }); }); - -describe('calculateDynamicSlippage - 1bp best bid/ask adjustment', () => { - const mockDriftClient = { - getMMOracleDataForPerpMarket: jest.fn(), - getOracleDataForSpotMarket: jest.fn(), - } as any; - - beforeEach(() => { - jest.clearAllMocks(); - // Set deterministic env values - process.env.DYNAMIC_BASE_SLIPPAGE_MAJOR = '0'; - process.env.DYNAMIC_SLIPPAGE_MULTIPLIER_MAJOR = '1'; - process.env.DYNAMIC_SLIPPAGE_MIN = '0.01'; // 0.01% minimum - process.env.DYNAMIC_SLIPPAGE_MAX = '100'; - }); - - it('should adjust slippage for LONG when impliedEndPrice < bestAskPrice', () => { - // Set very tight env to minimize base slippage - process.env.DYNAMIC_BASE_SLIPPAGE_MAJOR = '0'; - process.env.DYNAMIC_SLIPPAGE_MIN = '0'; - - const oraclePrice = new BN(100).mul(PRICE_PRECISION); // $100 - const bestBidPrice = new BN(100).mul(PRICE_PRECISION); // $100 (tight spread) - const bestAskPrice = new BN(101).mul(PRICE_PRECISION); // $101 - const startPrice = new BN(99).mul(PRICE_PRECISION); // $99 (start below ask) - - mockDriftClient.getMMOracleDataForPerpMarket.mockReturnValue({ - price: oraclePrice, - }); - - const l2 = { - bids: [{ price: bestBidPrice, size: new BN(1) }], - asks: [{ price: bestAskPrice, size: new BN(1) }], - } as any; - - // With start price at 99 and very small worst price, initial slippage will be tiny - const worstPrice = new BN(99100000); // $99.10 in PRICE_PRECISION - - const slippage = calculateDynamicSlippage( - 'long', - 0, // major perp - 'perp', - mockDriftClient, - l2, - startPrice, - worstPrice - ); - - // Calculate what the implied end price would be - const impliedEndPrice = startPrice.toNumber() * (1 + slippage / 100); - - // Verify that impliedEndPrice is at least 1bp greater than bestAskPrice - const targetPrice = bestAskPrice.toNumber() * 1.0001; // 1bp above bestAsk - expect(impliedEndPrice).toBeGreaterThanOrEqual(targetPrice); - - // With startPrice at 99 and bestAsk at 101, we need at least 2.02% slippage - // to reach 101 * 1.0001 = 101.0101 - expect(slippage).toBeGreaterThan(2.0); - expect(slippage).toBeLessThan(3.0); - }); - - it('should adjust slippage for SHORT when impliedEndPrice > bestBidPrice', () => { - // Set very tight env to minimize base slippage - process.env.DYNAMIC_BASE_SLIPPAGE_MAJOR = '0'; - process.env.DYNAMIC_SLIPPAGE_MIN = '0'; - - const oraclePrice = new BN(100).mul(PRICE_PRECISION); // $100 - const bestBidPrice = new BN(99).mul(PRICE_PRECISION); // $99 - const bestAskPrice = new BN(100).mul(PRICE_PRECISION); // $100 (tight spread) - const startPrice = new BN(101).mul(PRICE_PRECISION); // $101 (start above bid) - - mockDriftClient.getMMOracleDataForPerpMarket.mockReturnValue({ - price: oraclePrice, - }); - - const l2 = { - bids: [{ price: bestBidPrice, size: new BN(1) }], - asks: [{ price: bestAskPrice, size: new BN(1) }], - } as any; - - // With start price at 101 and very small worst price, initial slippage will be tiny - const worstPrice = new BN(100900000); // $100.90 in PRICE_PRECISION - - const slippage = calculateDynamicSlippage( - 'short', - 0, // major perp - 'perp', - mockDriftClient, - l2, - startPrice, - worstPrice - ); - - // Calculate what the implied end price would be - const impliedEndPrice = startPrice.toNumber() * (1 - slippage / 100); - - // Verify that impliedEndPrice is at least 1bp less than bestBidPrice - const targetPrice = bestBidPrice.toNumber() * 0.9999; // 1bp below bestBid - expect(impliedEndPrice).toBeLessThanOrEqual(targetPrice); - - // With startPrice at 101 and bestBid at 99, we need at least 1.99% slippage - // to reach 99 * 0.9999 = 98.9901 - expect(slippage).toBeGreaterThan(1.98); - expect(slippage).toBeLessThan(3.0); - }); - - it('should NOT adjust slippage for LONG when impliedEndPrice already > bestAskPrice', () => { - const oraclePrice = new BN(100).mul(PRICE_PRECISION); // $100 - const bestBidPrice = new BN(99).mul(PRICE_PRECISION); // $99 - const bestAskPrice = new BN(101).mul(PRICE_PRECISION); // $101 - const startPrice = new BN(100).mul(PRICE_PRECISION); // $100 - - mockDriftClient.getMMOracleDataForPerpMarket.mockReturnValue({ - price: oraclePrice, - }); - - const l2 = { - bids: [{ price: bestBidPrice, size: new BN(1) }], - asks: [{ price: bestAskPrice, size: new BN(1) }], - } as any; - - // With large worst price, impliedEndPrice will already be > bestAskPrice - const worstPrice = new BN(105).mul(PRICE_PRECISION); // $105 - large slippage - - const slippage = calculateDynamicSlippage( - 'long', - 0, // major perp - 'perp', - mockDriftClient, - l2, - startPrice, - worstPrice - ); - - // Calculate what the implied end price would be - const impliedEndPrice = startPrice.toNumber() * (1 + slippage / 100); - - // Should already be well above bestAskPrice - expect(impliedEndPrice).toBeGreaterThan(bestAskPrice.toNumber()); - - // Slippage should be driven by the size-adjusted calculation (halfway to worst) - // Expected: ((100 - 105) / 100 / 2) * 100 = 2.5% - expect(slippage).toBeGreaterThan(2.0); - expect(slippage).toBeLessThan(3.0); - }); - - it('should NOT adjust slippage for SHORT when impliedEndPrice already < bestBidPrice', () => { - const oraclePrice = new BN(100).mul(PRICE_PRECISION); // $100 - const bestBidPrice = new BN(99).mul(PRICE_PRECISION); // $99 - const bestAskPrice = new BN(101).mul(PRICE_PRECISION); // $101 - const startPrice = new BN(100).mul(PRICE_PRECISION); // $100 - - mockDriftClient.getMMOracleDataForPerpMarket.mockReturnValue({ - price: oraclePrice, - }); - - const l2 = { - bids: [{ price: bestBidPrice, size: new BN(1) }], - asks: [{ price: bestAskPrice, size: new BN(1) }], - } as any; - - // With large worst price, impliedEndPrice will already be < bestBidPrice - const worstPrice = new BN(95).mul(PRICE_PRECISION); // $95 - large slippage - - const slippage = calculateDynamicSlippage( - 'short', - 0, // major perp - 'perp', - mockDriftClient, - l2, - startPrice, - worstPrice - ); - - // Calculate what the implied end price would be - const impliedEndPrice = startPrice.toNumber() * (1 - slippage / 100); - - // Should already be well below bestBidPrice - expect(impliedEndPrice).toBeLessThan(bestBidPrice.toNumber()); - - // Slippage should be driven by the size-adjusted calculation - expect(slippage).toBeGreaterThan(2.0); - expect(slippage).toBeLessThan(3.0); - }); - - it('should handle tight spread with precise 1bp adjustment for LONG', () => { - const oraclePrice = new BN(100).mul(PRICE_PRECISION); // $100 - const bestBidPrice = new BN(99950000); // $99.95 in PRICE_PRECISION - const bestAskPrice = new BN(100050000); // $100.05 in PRICE_PRECISION (5bp spread) - const startPrice = new BN(100).mul(PRICE_PRECISION); // $100 - - mockDriftClient.getMMOracleDataForPerpMarket.mockReturnValue({ - price: oraclePrice, - }); - - const l2 = { - bids: [{ price: bestBidPrice, size: new BN(1) }], - asks: [{ price: bestAskPrice, size: new BN(1) }], - } as any; - - const worstPrice = new BN(100010000); // $100.01 in PRICE_PRECISION - - const slippage = calculateDynamicSlippage( - 'long', - 0, - 'perp', - mockDriftClient, - l2, - startPrice, - worstPrice - ); - - // Calculate implied end price - const impliedEndPrice = startPrice.toNumber() * (1 + slippage / 100); - const targetPrice = bestAskPrice.toNumber() * 1.0001; - - // Should be adjusted to go 1bp past bestAsk - expect(impliedEndPrice).toBeGreaterThanOrEqual(targetPrice); - - // The slippage will be influenced by spread calculation (5bp spread * 0.9 = 0.45%) - // plus size adjustment, so it will be higher than just the 1bp adjustment - expect(slippage).toBeGreaterThan(0.05); - expect(slippage).toBeLessThan(1.5); - }); - - it('should handle tight spread with precise 1bp adjustment for SHORT', () => { - const oraclePrice = new BN(100).mul(PRICE_PRECISION); // $100 - const bestBidPrice = new BN(99950000); // $99.95 in PRICE_PRECISION - const bestAskPrice = new BN(100050000); // $100.05 in PRICE_PRECISION (5bp spread) - const startPrice = new BN(100).mul(PRICE_PRECISION); // $100 - - mockDriftClient.getMMOracleDataForPerpMarket.mockReturnValue({ - price: oraclePrice, - }); - - const l2 = { - bids: [{ price: bestBidPrice, size: new BN(1) }], - asks: [{ price: bestAskPrice, size: new BN(1) }], - } as any; - - const worstPrice = new BN(99990000); // $99.99 in PRICE_PRECISION - - const slippage = calculateDynamicSlippage( - 'short', - 0, - 'perp', - mockDriftClient, - l2, - startPrice, - worstPrice - ); - - // Calculate implied end price - const impliedEndPrice = startPrice.toNumber() * (1 - slippage / 100); - const targetPrice = bestBidPrice.toNumber() * 0.9999; - - // Should be adjusted to go 1bp past bestBid - expect(impliedEndPrice).toBeLessThanOrEqual(targetPrice); - - // The slippage will be influenced by spread calculation (5bp spread * 0.9 = 0.45%) - // plus the 1bp adjustment needed, so it will be higher than just 0.06% - expect(slippage).toBeGreaterThan(0.05); - expect(slippage).toBeLessThan(1.5); - }); - - it('should handle crossed orderbook for LONG', () => { - const oraclePrice = new BN(100).mul(PRICE_PRECISION); // $100 - const bestBidPrice = new BN(101).mul(PRICE_PRECISION); // $101 (crossed) - const bestAskPrice = new BN(99).mul(PRICE_PRECISION); // $99 (crossed) - const startPrice = new BN(100).mul(PRICE_PRECISION); // $100 - - mockDriftClient.getMMOracleDataForPerpMarket.mockReturnValue({ - price: oraclePrice, - }); - - const l2 = { - bids: [{ price: bestBidPrice, size: new BN(1) }], - asks: [{ price: bestAskPrice, size: new BN(1) }], - } as any; - - const worstPrice = new BN(100010000); // $100.01 in PRICE_PRECISION - - const slippage = calculateDynamicSlippage( - 'long', - 0, - 'perp', - mockDriftClient, - l2, - startPrice, - worstPrice - ); - - // Even with crossed book, should still calculate correctly - // bestAskPrice is 99, so targetPrice = 99 * 1.0001 = 99.0099 - // impliedEndPrice should be >= 99.0099 - const impliedEndPrice = startPrice.toNumber() * (1 + slippage / 100); - - // Since bestAsk (99) < startPrice (100), the adjustment shouldn't trigger - // because impliedEndPrice will already be > bestAsk - expect(impliedEndPrice).toBeGreaterThan(bestAskPrice.toNumber()); - }); - - it('should respect minimum slippage even with adjustment', () => { - process.env.DYNAMIC_SLIPPAGE_MIN = '2.0'; // 2% minimum - - const oraclePrice = new BN(100).mul(PRICE_PRECISION); - const bestBidPrice = new BN(99).mul(PRICE_PRECISION); - const bestAskPrice = new BN(101).mul(PRICE_PRECISION); - const startPrice = new BN(100).mul(PRICE_PRECISION); - - mockDriftClient.getMMOracleDataForPerpMarket.mockReturnValue({ - price: oraclePrice, - }); - - const l2 = { - bids: [{ price: bestBidPrice, size: new BN(1) }], - asks: [{ price: bestAskPrice, size: new BN(1) }], - } as any; - - const worstPrice = new BN(100010000); // $100.01 in PRICE_PRECISION - - const slippage = calculateDynamicSlippage( - 'long', - 0, - 'perp', - mockDriftClient, - l2, - startPrice, - worstPrice - ); - - // Should respect the 2% minimum - expect(slippage).toBeGreaterThanOrEqual(2.0); - }); -}); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index eaff816..e90aa66 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1172,7 +1172,6 @@ export const mapToMarketOrderParams = async ( const startPrice = estimatedPrices[startPriceProperty]; processedSlippageTolerance = calculateDynamicSlippage( - direction === PositionDirection.LONG ? 'long' : 'short', params.marketIndex, params.marketType, driftClient, @@ -1378,7 +1377,6 @@ export const fetchL2FromRedis = async ( * @returns Dynamic slippage tolerance as a number */ export const calculateDynamicSlippage = ( - direction: 'long' | 'short', marketIndex: number, marketType: string, driftClient: DriftClient, @@ -1399,6 +1397,7 @@ export const calculateDynamicSlippage = ( // Calculate spread using L2 data let spreadBaseSlippage = 0.0005; // 0.05% fallback spread + try { // Get oracle data const oracleData = isPerp ? driftClient.getMMOracleDataForPerpMarket(marketIndex) @@ -1413,8 +1412,6 @@ export const calculateDynamicSlippage = ( oraclePrice ); - - try { const spreadPctNum = BigNum.from( spreadInfo.spreadPct, PERCENTAGE_PRECISION_EXP @@ -1467,39 +1464,7 @@ export const calculateDynamicSlippage = ( const minSlippage = parseFloat(process.env.DYNAMIC_SLIPPAGE_MIN || '0.035'); // 0.035% minimum const maxSlippage = parseFloat(process.env.DYNAMIC_SLIPPAGE_MAX || '5'); // 5% maximum - let finalSlippage = Math.min(Math.max(dynamicSlippage, minSlippage), maxSlippage); - - // make sure the slippage goes at least 1bp past the bestBid/bestAsk - const impliedEndPriceNum = direction === 'long' ? (startPrice.toNumber() * (1 + finalSlippage / 100)) : (startPrice.toNumber() * (1 - finalSlippage / 100)); - - const impliedEndPrice = new BN(impliedEndPriceNum); - - // Adjust slippage to ensure it goes 1bp past best bid/ask if needed - try { - const ONE_BP = 0.0001; // 1 basis point = 0.01% - - if (direction === 'long' && spreadInfo.bestAskPrice) { - // For LONG: if impliedEndPrice < bestAskPrice, adjust so startPrice + finalSlippage is 1bp greater than bestAskPrice - if (impliedEndPrice.lt(spreadInfo.bestAskPrice)) { - // Calculate required slippage: ((bestAskPrice * (1 + 1bp)) / startPrice - 1) * 100 - const targetPrice = spreadInfo.bestAskPrice.toNumber() * (1 + ONE_BP); - const requiredSlippagePct = ((targetPrice / startPrice.toNumber()) - 1) * 100; - finalSlippage = Math.max(finalSlippage, requiredSlippagePct); - } - } else if (direction === 'short' && spreadInfo.bestBidPrice) { - // For SHORT: if impliedEndPrice > bestBidPrice, adjust so startPrice - finalSlippage is 1bp less than bestBidPrice - if (impliedEndPrice.gt(spreadInfo.bestBidPrice)) { - // Calculate required slippage: (1 - (bestBidPrice * (1 - 1bp)) / startPrice) * 100 - const targetPrice = spreadInfo.bestBidPrice.toNumber() * (1 - ONE_BP); - const requiredSlippagePct = (1 - (targetPrice / startPrice.toNumber())) * 100; - finalSlippage = Math.max(finalSlippage, requiredSlippagePct); - } - } - } catch (error) { - logger.error('Failed to adjust slippage for best bid/ask:', error); - } - - return finalSlippage; + return Math.min(Math.max(dynamicSlippage, minSlippage), maxSlippage); }; /**