diff --git a/src/utils/tests/auctionParams.test.ts b/src/utils/tests/auctionParams.test.ts index f9f4b2b..5bfad84 100644 --- a/src/utils/tests/auctionParams.test.ts +++ b/src/utils/tests/auctionParams.test.ts @@ -727,6 +727,7 @@ describe('calculateDynamicSlippage - crossed book handling', () => { const worstPrice = new BN(100).mul(PRICE_PRECISION); const slip = calculateDynamicSlippage( + 'long', 0, // major perp 'perp', mockDriftClient, @@ -761,6 +762,7 @@ describe('calculateDynamicSlippage - crossed book handling', () => { const worstPrice = new BN(100).mul(PRICE_PRECISION); const slipNormal = calculateDynamicSlippage( + 'long', 0, 'perp', mockDriftClient, @@ -775,6 +777,7 @@ describe('calculateDynamicSlippage - crossed book handling', () => { } as any; const slipCrossed = calculateDynamicSlippage( + 'long', 0, 'perp', mockDriftClient, @@ -787,3 +790,338 @@ 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 e90aa66..eaff816 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1172,6 +1172,7 @@ export const mapToMarketOrderParams = async ( const startPrice = estimatedPrices[startPriceProperty]; processedSlippageTolerance = calculateDynamicSlippage( + direction === PositionDirection.LONG ? 'long' : 'short', params.marketIndex, params.marketType, driftClient, @@ -1377,6 +1378,7 @@ export const fetchL2FromRedis = async ( * @returns Dynamic slippage tolerance as a number */ export const calculateDynamicSlippage = ( + direction: 'long' | 'short', marketIndex: number, marketType: string, driftClient: DriftClient, @@ -1397,7 +1399,6 @@ 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) @@ -1412,6 +1413,8 @@ export const calculateDynamicSlippage = ( oraclePrice ); + + try { const spreadPctNum = BigNum.from( spreadInfo.spreadPct, PERCENTAGE_PRECISION_EXP @@ -1464,7 +1467,39 @@ 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 - return Math.min(Math.max(dynamicSlippage, minSlippage), maxSlippage); + 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; }; /**