Auction Params Endpoint (#442)

* move auction endpoint changes to fresh branch

* final cleanup and add testing for util functions

* fix tests

* fix quote amount conversion
This commit is contained in:
lowkeynicc
2025-07-14 19:13:43 -04:00
committed by GitHub
parent dd524de371
commit 7483704d09
7 changed files with 2553 additions and 35 deletions

View File

@@ -43,6 +43,8 @@
"ws": "^8.14.2" "ws": "^8.14.2"
}, },
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.3.1",
"@types/jest": "^29.4.0",
"@types/k6": "^0.45.0", "@types/k6": "^0.45.0",
"@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0", "@typescript-eslint/parser": "^4.28.0",
@@ -51,9 +53,11 @@
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"husky": "^7.0.4", "husky": "^7.0.4",
"jest": "^29.7.0",
"k6": "^0.0.0", "k6": "^0.0.0",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"tiny-glob": "^0.2.9", "tiny-glob": "^0.2.9",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
}, },
"scripts": { "scripts": {
@@ -77,7 +81,27 @@
"prettify:fix": "prettier --write './src/**/*.ts'", "prettify:fix": "prettier --write './src/**/*.ts'",
"lint": "eslint . --ext ts --quiet", "lint": "eslint . --ext ts --quiet",
"lint:fix": "eslint . --ext ts --fix", "lint:fix": "eslint . --ext ts --fix",
"playground": "ts-node src/playground.ts" "playground": "ts-node src/playground.ts",
"test": "jest src/utils/tests/auctionParams.test.ts",
"test:watch": "jest src/utils/tests/auctionParams.test.ts --watch"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"moduleFileExtensions": [
"ts",
"js"
],
"transform": {
"^.+\\.ts$": "ts-jest"
},
"testMatch": [
"**/*.test.ts"
],
"moduleNameMapper": {
"^@drift-labs/sdk$": "<rootDir>/drift-common/protocol/sdk/lib/node/index.js",
"^@drift/common$": "<rootDir>/drift-common/common-ts/lib/index.js"
}
}, },
"resolutions": { "resolutions": {
"rpc-websockets": "^10.0.0" "rpc-websockets": "^10.0.0"

View File

@@ -18,6 +18,10 @@ import {
isVariant, isVariant,
OrderSubscriber, OrderSubscriber,
DelistedMarketSetting, DelistedMarketSetting,
BigNum,
PRICE_PRECISION_EXP,
MarketTypeStr,
AssetType,
} from '@drift-labs/sdk'; } from '@drift-labs/sdk';
import { RedisClient, RedisClientPrefix } from '@drift/common/clients'; import { RedisClient, RedisClientPrefix } from '@drift/common/clients';
@@ -41,11 +45,19 @@ import {
getAccountFromId, getAccountFromId,
getRawAccountFromId, getRawAccountFromId,
selectMostRecentBySlot, selectMostRecentBySlot,
createMarketBasedAuctionParams,
parseBoolean,
parseNumber,
mapToMarketOrderParams,
formatAuctionParamsForResponse,
fetchL2FromRedis,
} from './utils/utils'; } from './utils/utils';
import FEATURE_FLAGS from './utils/featureFlags'; import FEATURE_FLAGS from './utils/featureFlags';
import { getDLOBProviderFromOrderSubscriber } from './dlobProvider'; import { getDLOBProviderFromOrderSubscriber } from './dlobProvider';
import { setGlobalDispatcher, Agent } from 'undici'; import { setGlobalDispatcher, Agent } from 'undici';
import { HermesClient } from '@pythnetwork/hermes-client'; import { HermesClient } from '@pythnetwork/hermes-client';
import { COMMON_UI_UTILS } from '@drift/common';
import { AuctionParamArgs } from './utils/types';
setGlobalDispatcher( setGlobalDispatcher(
new Agent({ new Agent({
@@ -546,11 +558,12 @@ const main = async (): Promise<void> => {
const adjustedDepth = depth ?? '100'; const adjustedDepth = depth ?? '100';
let l2Formatted: any; let l2Formatted: any;
const redisL2 = await fetchFromRedis( const redisL2 = await fetchL2FromRedis(
`last_update_orderbook_${ fetchFromRedis,
isSpot ? 'spot' : 'perp' selectMostRecentBySlot,
}_${normedMarketIndex}${includeIndicativeStr ? '_indicative' : ''}`, normedMarketType,
selectMostRecentBySlot normedMarketIndex,
includeIndicativeStr
); );
const depthToUse = Math.min(parseInt(adjustedDepth as string) ?? 1, 100); const depthToUse = Math.min(parseInt(adjustedDepth as string) ?? 1, 100);
let cacheMiss = true; let cacheMiss = true;
@@ -660,13 +673,12 @@ const main = async (): Promise<void> => {
const adjustedDepth = normedParam['depth'] ?? '100'; const adjustedDepth = normedParam['depth'] ?? '100';
let l2Formatted: any; let l2Formatted: any;
const redisL2 = await fetchFromRedis( const redisL2 = await fetchL2FromRedis(
`last_update_orderbook_${ fetchFromRedis,
isSpot ? 'spot' : 'perp' selectMostRecentBySlot,
}_${normedMarketIndex}${ normedMarketType,
normedIncludeIndicative ? '_indicative' : '' normedMarketIndex,
}`, normedIncludeIndicative
selectMostRecentBySlot
); );
const depth = Math.min(parseInt(adjustedDepth as string) ?? 1, 100); const depth = Math.min(parseInt(adjustedDepth as string) ?? 1, 100);
let cacheMiss = true; let cacheMiss = true;
@@ -903,6 +915,139 @@ const main = async (): Promise<void> => {
} }
}); });
app.get('/auctionParams', async (req, res, next) => {
try {
const {
marketIndex,
marketType,
direction,
amount,
assetType,
reduceOnly,
allowInfSlippage,
slippageTolerance,
isOracleOrder,
auctionDuration,
auctionStartPriceOffset,
auctionEndPriceOffset,
auctionStartPriceOffsetFrom,
auctionEndPriceOffsetFrom,
additionalEndPriceBuffer,
userOrderId,
} = req.query;
// Validate required parameters
if (!marketIndex || !marketType || !direction || !amount || !assetType) {
res
.status(400)
.send(
'Bad Request: marketIndex, marketType, direction, amount, and assetType are required'
);
return;
}
// Parse and validate values
const parsedMarketIndex = parseInt(marketIndex as string);
if (isNaN(parsedMarketIndex)) {
res.status(400).send('Bad Request: marketIndex must be a valid number');
return;
}
if (direction !== 'long' && direction !== 'short') {
res
.status(400)
.send('Bad Request: direction must be either "long" or "short"');
return;
}
if (assetType !== 'base' && assetType !== 'quote') {
res
.status(400)
.send('Bad Request: assetType must be either "base" or "quote"');
return;
}
// Build auction params object
const auctionParamsInput: AuctionParamArgs = {
marketIndex: parsedMarketIndex,
marketType: marketType as MarketTypeStr,
direction: direction as 'long' | 'short',
amount: amount as string,
assetType: assetType as AssetType,
};
// Add optional parameters if provided
const optionalParams = {
reduceOnly: parseBoolean(reduceOnly as string),
allowInfSlippage: parseBoolean(allowInfSlippage as string),
slippageTolerance:
slippageTolerance === 'dynamic'
? undefined
: parseNumber(slippageTolerance as string), // Convert "dynamic" to undefined for dynamic calculation
isOracleOrder: parseBoolean(isOracleOrder as string),
auctionDuration: parseNumber(auctionDuration as string),
auctionStartPriceOffset:
auctionStartPriceOffset === 'marketBased'
? 'marketBased'
: parseNumber(auctionStartPriceOffset as string),
auctionEndPriceOffset: parseNumber(auctionEndPriceOffset as string),
auctionStartPriceOffsetFrom:
auctionStartPriceOffsetFrom === 'marketBased'
? 'marketBased'
: (auctionStartPriceOffsetFrom as any),
auctionEndPriceOffsetFrom: auctionEndPriceOffsetFrom as any,
additionalEndPriceBuffer: additionalEndPriceBuffer as string,
userOrderId: parseNumber(userOrderId as string),
};
// Only add non-undefined values
Object.entries(optionalParams).forEach(([key, value]) => {
if (value !== undefined) {
auctionParamsInput[key] = value;
}
});
const inputParams = createMarketBasedAuctionParams(auctionParamsInput);
const result = await mapToMarketOrderParams(
inputParams,
driftClient,
fetchFromRedis,
selectMostRecentBySlot
);
if (!result.success) {
res.status(400).json({
error: result.error,
});
return;
}
const auctionParams = COMMON_UI_UTILS.deriveMarketOrderParams(
result.data.marketOrderParams
);
const response = {
data: {
params: formatAuctionParamsForResponse(auctionParams),
entryPrice: result.data.estimatedPrices.entryPrice.toString(),
bestPrice: result.data.estimatedPrices.bestPrice.toString(),
worstPrice: result.data.estimatedPrices.worstPrice.toString(),
priceImpact: BigNum.from(
result.data.estimatedPrices.priceImpact,
PRICE_PRECISION_EXP
).toNum(),
slippageTolerance: result.data.marketOrderParams.slippageTolerance,
baseFilled: result.data.estimatedPrices.baseFilled,
},
};
res.status(200).json(response);
} catch (err) {
next(err);
}
});
server.listen(serverPort, () => { server.listen(serverPort, () => {
logger.info(`DLOB server listening on port http://localhost:${serverPort}`); logger.info(`DLOB server listening on port http://localhost:${serverPort}`);
}); });
@@ -920,4 +1065,4 @@ async function recursiveTryCatch(f: () => Promise<void>) {
recursiveTryCatch(() => main()); recursiveTryCatch(() => main());
export { commitHash, driftClient, driftEnv, endpoint, sdkConfig, wsEndpoint }; export { commitHash, driftClient, driftEnv, endpoint, sdkConfig, wsEndpoint };

View File

@@ -1,3 +1,5 @@
import { AuctionParamArgs } from "./types";
export const MEASURED_ENDPOINTS = [ export const MEASURED_ENDPOINTS = [
'/priorityFees', '/priorityFees',
'/batchPriorityFees', '/batchPriorityFees',
@@ -7,3 +9,17 @@ export const MEASURED_ENDPOINTS = [
'/batchL2Cache', '/batchL2Cache',
'/l3', '/l3',
]; ];
export const DEFAULT_MARKET_AUCTION_DURATION = 20;
export const DEFAULT_LIMIT_AUCTION_DURATION = 60;
const DEFAULT_AUCTION_END_PRICE_OFFSET = 0.1;
const DEFAULT_AUCTION_END_PRICE_FROM = 'worst';
export const DEFAULT_AUCTION_PARAMS: Partial<AuctionParamArgs> = {
isOracleOrder: true,
auctionDuration: DEFAULT_MARKET_AUCTION_DURATION,
auctionStartPriceOffset: 'marketBased',
auctionEndPriceOffset: DEFAULT_AUCTION_END_PRICE_OFFSET,
auctionStartPriceOffsetFrom: 'marketBased',
auctionEndPriceOffsetFrom: DEFAULT_AUCTION_END_PRICE_FROM,
};

View File

@@ -0,0 +1,682 @@
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
import {
BN,
PositionDirection,
MarketType,
ZERO,
QUOTE_PRECISION,
PRICE_PRECISION
} from '@drift-labs/sdk';
import {
createMarketBasedAuctionParams,
mapToMarketOrderParams,
formatAuctionParamsForResponse,
} from '../utils';
describe('Auction Parameters Functions', () => {
describe('createMarketBasedAuctionParams', () => {
it('should apply market-based logic for major PERP markets (0, 1, 2)', () => {
[0, 1, 2].forEach(marketIndex => {
const args = {
marketIndex,
marketType: 'perp' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
auctionStartPriceOffsetFrom: 'marketBased' as any,
auctionStartPriceOffset: 'marketBased' as any,
};
const result = createMarketBasedAuctionParams(args);
expect(result.auctionStartPriceOffsetFrom).toBe('mark');
expect(result.auctionStartPriceOffset).toBe(0);
});
});
it('should apply market-based logic for minor PERP markets (>2)', () => {
[3, 4, 5, 10].forEach(marketIndex => {
const args = {
marketIndex,
marketType: 'perp' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
auctionStartPriceOffsetFrom: 'marketBased' as any,
auctionStartPriceOffset: 'marketBased' as any,
};
const result = createMarketBasedAuctionParams(args);
expect(result.auctionStartPriceOffsetFrom).toBe('bestOffer');
expect(result.auctionStartPriceOffset).toBe(-0.1);
});
});
it('should apply market-based logic for SPOT markets when "marketBased" is passed', () => {
[0, 1, 2, 5, 10].forEach(marketIndex => {
const args = {
marketIndex,
marketType: 'spot' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
auctionStartPriceOffsetFrom: 'marketBased' as any,
auctionStartPriceOffset: 'marketBased' as any,
};
const result = createMarketBasedAuctionParams(args);
expect(result.auctionStartPriceOffsetFrom).toBe('bestOffer');
expect(result.auctionStartPriceOffset).toBe(-0.1);
});
});
it('should treat undefined values the same as "marketBased"', () => {
const majorMarketArgs = {
marketIndex: 0,
marketType: 'perp' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
// auctionStartPriceOffsetFrom and auctionStartPriceOffset are undefined
};
const minorMarketArgs = {
marketIndex: 5,
marketType: 'perp' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
// auctionStartPriceOffsetFrom and auctionStartPriceOffset are undefined
};
const majorResult = createMarketBasedAuctionParams(majorMarketArgs);
const minorResult = createMarketBasedAuctionParams(minorMarketArgs);
// Major market behavior
expect(majorResult.auctionStartPriceOffsetFrom).toBe('mark');
expect(majorResult.auctionStartPriceOffset).toBe(0);
// Minor market behavior
expect(minorResult.auctionStartPriceOffsetFrom).toBe('bestOffer');
expect(minorResult.auctionStartPriceOffset).toBe(-0.1);
});
it('should preserve explicit values over market-based defaults', () => {
const args = {
marketIndex: 0, // Major market
marketType: 'perp' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
auctionStartPriceOffsetFrom: 'oracle' as any,
auctionStartPriceOffset: 0.5,
auctionEndPriceOffset: 0.2,
auctionDuration: 45,
};
const result = createMarketBasedAuctionParams(args);
// Should use explicit values, not market-based defaults
expect(result.auctionStartPriceOffsetFrom).toBe('oracle');
expect(result.auctionStartPriceOffset).toBe(0.5);
expect(result.auctionEndPriceOffset).toBe(0.2);
expect(result.auctionDuration).toBe(45);
});
it('should handle mixed explicit and market-based values', () => {
const args = {
marketIndex: 0,
marketType: 'perp' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
auctionStartPriceOffsetFrom: 'oracle' as any, // explicit
auctionStartPriceOffset: 'marketBased' as any, // market-based
auctionDuration: 45, // explicit
};
const result = createMarketBasedAuctionParams(args);
expect(result.auctionStartPriceOffsetFrom).toBe('oracle'); // explicit value
expect(result.auctionStartPriceOffset).toBe(0); // market-based for major market
expect(result.auctionDuration).toBe(45); // explicit value
});
it('should include all original parameters in the result', () => {
const args = {
marketIndex: 3,
marketType: 'perp' as any,
direction: 'short' as any,
amount: '250',
assetType: 'quote' as any,
reduceOnly: true,
allowInfSlippage: false,
slippageTolerance: 0.02,
userOrderId: 12345,
auctionStartPriceOffsetFrom: 'marketBased' as any,
auctionStartPriceOffset: 'marketBased' as any,
};
const result = createMarketBasedAuctionParams(args);
// Should preserve all original parameters
expect(result.marketIndex).toBe(3);
expect(result.marketType).toBe('perp');
expect(result.direction).toBe('short');
expect(result.amount).toBe('250');
expect(result.assetType).toBe('quote');
expect(result.reduceOnly).toBe(true);
expect(result.allowInfSlippage).toBe(false);
expect(result.slippageTolerance).toBe(0.02);
expect(result.userOrderId).toBe(12345);
// Should apply market-based logic
expect(result.auctionStartPriceOffsetFrom).toBe('bestOffer');
expect(result.auctionStartPriceOffset).toBe(-0.1);
});
});
describe('mapToMarketOrderParams with Mock L2 Data', () => {
const mockDriftClient = {
getOracleDataForPerpMarket: jest.fn(),
getOracleDataForSpotMarket: jest.fn(),
};
const mockFetchFromRedis = jest.fn() as any;
const mockSelectMostRecentBySlot = jest.fn() as any;
const validParams = {
marketIndex: 0,
marketType: 'perp' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
auctionDuration: 30,
auctionStartPriceOffset: 0,
auctionEndPriceOffset: 0.1,
auctionStartPriceOffsetFrom: 'mark' as any,
auctionEndPriceOffsetFrom: 'mark' as any,
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should successfully calculate prices with mock L2 orderbook data', async () => {
const solPrice = 160; // $160 SOL price
mockDriftClient.getOracleDataForPerpMarket.mockReturnValue({
price: new BN(solPrice).mul(PRICE_PRECISION),
});
// Mock realistic L2 orderbook data
mockFetchFromRedis.mockImplementation(async (key: string) => {
if (key.includes('dlob_orderbook')) {
return {
bids: [
{ price: '159950000', size: '1000000000' }, // $159.95, 1 SOL
{ price: '159900000', size: '2000000000' }, // $159.90, 2 SOL
{ price: '159850000', size: '1500000000' }, // $159.85, 1.5 SOL
],
asks: [
{ price: '160050000', size: '1500000000' }, // $160.05, 1.5 SOL
{ price: '160100000', size: '1000000000' }, // $160.10, 1 SOL
{ price: '160150000', size: '2000000000' }, // $160.15, 2 SOL
],
};
}
return null;
});
const result = await mapToMarketOrderParams(
validParams,
mockDriftClient as any,
mockFetchFromRedis as any,
mockSelectMostRecentBySlot as any
);
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data?.marketOrderParams).toBeDefined();
expect(result.data?.estimatedPrices).toBeDefined();
expect(result.error).toBeUndefined();
// Verify price calculations
const prices = result.data?.estimatedPrices;
expect(prices?.entryPrice.gt(ZERO)).toBe(true);
expect(prices?.bestPrice.gt(ZERO)).toBe(true);
expect(prices?.worstPrice.gt(ZERO)).toBe(true);
expect(prices?.oraclePrice.gt(ZERO)).toBe(true);
// Verify market order params structure
const marketOrderParams = result.data?.marketOrderParams;
expect(marketOrderParams?.marketType).toBe(MarketType.PERP);
expect(marketOrderParams?.direction).toBe(PositionDirection.LONG);
expect(marketOrderParams?.baseAmount).toBeDefined();
expect(marketOrderParams?.baseAmount.gt(ZERO)).toBe(true);
});
it('should handle quote-to-base conversion with realistic prices', async () => {
const solPrice = 160; // $160 SOL price
const quoteAmount = 1000; // $1,000 worth
mockDriftClient.getOracleDataForPerpMarket.mockReturnValue({
price: new BN(solPrice).mul(PRICE_PRECISION),
});
mockFetchFromRedis.mockImplementation(async (key: string) => {
if (key.includes('dlob_orderbook')) {
return {
bids: [
{ price: '160000000', size: '1000000000' }, // $160.00, 1 SOL
],
asks: [
{ price: '160000000', size: '1000000000' }, // $160.00, 1 SOL
],
};
}
return null;
});
const quoteParams = {
...validParams,
amount: quoteAmount.toString(),
assetType: 'quote' as any,
};
const result = await mapToMarketOrderParams(
quoteParams,
mockDriftClient as any,
mockFetchFromRedis as any,
mockSelectMostRecentBySlot as any
);
expect(result.success).toBe(true);
// With $1,000 at $160 per SOL, we should get ~6.25 SOL
const baseAmount = result.data?.marketOrderParams.baseAmount;
expect(baseAmount).toBeDefined();
expect(baseAmount.gt(ZERO)).toBe(true);
// Calculate expected base amount: (quoteAmount * QUOTE_PRECISION * BASE_PRECISION) / entryPrice
const expectedBaseAmount = new BN(quoteAmount)
.mul(QUOTE_PRECISION)
.mul(new BN(10).pow(new BN(9))) // BASE_PRECISION = 10^9
.div(result.data?.estimatedPrices.entryPrice);
expect(baseAmount.toString()).toBe(expectedBaseAmount.toString());
});
it('should handle different market types correctly', async () => {
const usdcPrice = 1; // $1 USDC price
mockDriftClient.getOracleDataForSpotMarket.mockReturnValue({
price: new BN(usdcPrice).mul(PRICE_PRECISION),
});
mockFetchFromRedis.mockImplementation(async (key: string) => {
if (key.includes('dlob_orderbook')) {
return {
bids: [
{ price: '1000000', size: '10000000000' }, // $1.00, 10,000 USDC
],
asks: [
{ price: '1000000', size: '10000000000' }, // $1.00, 10,000 USDC
],
};
}
return null;
});
const spotParams = {
...validParams,
marketType: 'spot' as any,
};
const result = await mapToMarketOrderParams(
spotParams,
mockDriftClient as any,
mockFetchFromRedis as any,
mockSelectMostRecentBySlot as any
);
expect(result.success).toBe(true);
expect(result.data?.marketOrderParams.marketType).toBe(MarketType.SPOT);
expect(result.data?.estimatedPrices.entryPrice.gt(ZERO)).toBe(true);
});
it('should handle different directions correctly', async () => {
const solPrice = 160; // $160 SOL price
mockDriftClient.getOracleDataForPerpMarket.mockReturnValue({
price: new BN(solPrice).mul(PRICE_PRECISION),
});
mockFetchFromRedis.mockImplementation(async (key: string) => {
if (key.includes('dlob_orderbook')) {
return {
bids: [
{ price: '159950000', size: '1000000000' }, // $159.95, 1 SOL
],
asks: [
{ price: '160050000', size: '1000000000' }, // $160.05, 1 SOL
],
};
}
return null;
});
const shortParams = {
...validParams,
direction: 'short' as any,
};
const result = await mapToMarketOrderParams(
shortParams,
mockDriftClient as any,
mockFetchFromRedis as any,
mockSelectMostRecentBySlot as any
);
expect(result.success).toBe(true);
expect(result.data?.marketOrderParams.direction).toBe(PositionDirection.SHORT);
expect(result.data?.estimatedPrices.entryPrice.gt(ZERO)).toBe(true);
});
it('should handle various order sizes with L2 depth', async () => {
const solPrice = 160; // $160 SOL price
mockDriftClient.getOracleDataForPerpMarket.mockReturnValue({
price: new BN(solPrice).mul(PRICE_PRECISION),
});
// Mock L2 with multiple levels of depth
mockFetchFromRedis.mockImplementation(async (key: string) => {
if (key.includes('dlob_orderbook')) {
return {
bids: [
{ price: '159950000', size: '500000000' }, // $159.95, 0.5 SOL
{ price: '159900000', size: '1000000000' }, // $159.90, 1 SOL
{ price: '159850000', size: '2000000000' }, // $159.85, 2 SOL
],
asks: [
{ price: '160050000', size: '500000000' }, // $160.05, 0.5 SOL
{ price: '160100000', size: '1000000000' }, // $160.10, 1 SOL
{ price: '160150000', size: '2000000000' }, // $160.15, 2 SOL
],
};
}
return null;
});
// Test small order
const smallOrderParams = {
...validParams,
amount: '10', // Small order
};
const smallResult = await mapToMarketOrderParams(
smallOrderParams,
mockDriftClient as any,
mockFetchFromRedis as any,
mockSelectMostRecentBySlot as any
);
expect(smallResult.success).toBe(true);
expect(smallResult.data?.estimatedPrices.entryPrice.gt(ZERO)).toBe(true);
// Test large order
const largeOrderParams = {
...validParams,
amount: '1000', // Large order
};
const largeResult = await mapToMarketOrderParams(
largeOrderParams,
mockDriftClient as any,
mockFetchFromRedis as any,
mockSelectMostRecentBySlot as any
);
expect(largeResult.success).toBe(true);
expect(largeResult.data?.estimatedPrices.entryPrice.gt(ZERO)).toBe(true);
// Large orders should have higher price impact
expect(largeResult.data?.estimatedPrices.priceImpact.gte(smallResult.data?.estimatedPrices.priceImpact)).toBe(true);
});
it('should handle zero oracle price scenario', async () => {
mockDriftClient.getOracleDataForPerpMarket.mockReturnValue({
price: ZERO,
});
mockFetchFromRedis.mockImplementation(async (key: string) => {
if (key.includes('dlob_orderbook')) {
return {
bids: [
{ price: '159950000', size: '1000000000' }, // $159.95, 1 SOL
],
asks: [
{ price: '160050000', size: '1000000000' }, // $160.05, 1 SOL
],
};
}
return null;
});
const result = await mapToMarketOrderParams(
validParams,
mockDriftClient as any,
mockFetchFromRedis as any,
mockSelectMostRecentBySlot as any
);
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data?.estimatedPrices.oraclePrice.eq(ZERO)).toBe(true);
// Entry price should be calculated from L2 data even if oracle price is zero
expect(result.data?.estimatedPrices.entryPrice).toBeDefined();
});
});
describe('formatAuctionParamsForResponse', () => {
it('should format BN values to strings', () => {
const mockAuctionParams = {
auctionStartPriceOffsetFrom: 'mark',
auctionStartPriceOffset: new BN(0),
auctionEndPriceOffsetFrom: 'best',
auctionEndPriceOffset: new BN(100000), // 0.1 with PRICE_PRECISION (1e6)
auctionDuration: 30,
slippageTolerance: 0.05,
additionalEndPriceBuffer: new BN(50000), // 0.05 with PRICE_PRECISION
userOrderId: 123,
isOracleOrder: false,
};
const result = formatAuctionParamsForResponse(mockAuctionParams);
expect(result.auctionStartPriceOffsetFrom).toBe('mark');
expect(result.auctionStartPriceOffset).toBe('0');
expect(result.auctionEndPriceOffsetFrom).toBe('best');
expect(result.auctionEndPriceOffset).toBe('100000');
expect(result.auctionDuration).toBe(30);
expect(result.slippageTolerance).toBe(0.05);
expect(result.additionalEndPriceBuffer).toBe('50000');
expect(result.userOrderId).toBe(123);
expect(result.isOracleOrder).toBe(false);
});
it('should handle different BN values correctly', () => {
const mockAuctionParams = {
auctionStartPriceOffsetFrom: 'oracle',
auctionStartPriceOffset: new BN(250000), // 0.25 with PRICE_PRECISION
auctionEndPriceOffsetFrom: 'worst',
auctionEndPriceOffset: new BN(1500000), // 1.5 with PRICE_PRECISION
auctionDuration: 60,
slippageTolerance: 0.1,
additionalEndPriceBuffer: new BN(0), // 0 with PRICE_PRECISION
userOrderId: 456,
isOracleOrder: true,
};
const result = formatAuctionParamsForResponse(mockAuctionParams);
expect(result.auctionStartPriceOffset).toBe('250000');
expect(result.auctionEndPriceOffset).toBe('1500000');
expect(result.additionalEndPriceBuffer).toBe('0');
expect(result.isOracleOrder).toBe(true);
});
it('should handle undefined/null values gracefully', () => {
const mockAuctionParams = {
auctionStartPriceOffsetFrom: 'mark',
auctionStartPriceOffset: new BN(0),
auctionEndPriceOffsetFrom: 'mark',
auctionEndPriceOffset: new BN(100000),
auctionDuration: 30,
slippageTolerance: undefined,
additionalEndPriceBuffer: undefined,
userOrderId: undefined,
isOracleOrder: undefined,
};
const result = formatAuctionParamsForResponse(mockAuctionParams);
expect(result.auctionStartPriceOffsetFrom).toBe('mark');
expect(result.auctionStartPriceOffset).toBe('0');
expect(result.auctionEndPriceOffsetFrom).toBe('mark');
expect(result.auctionEndPriceOffset).toBe('100000');
expect(result.auctionDuration).toBe(30);
expect(result.slippageTolerance).toBeUndefined();
expect(result.additionalEndPriceBuffer).toBeUndefined();
expect(result.userOrderId).toBeUndefined();
expect(result.isOracleOrder).toBeUndefined();
});
});
describe('Business Logic Integration', () => {
it('should demonstrate complete parameter flow for different market types', () => {
const scenarios = [
{
name: 'Major PERP market with market-based params',
input: {
marketIndex: 0,
marketType: 'perp' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
auctionStartPriceOffsetFrom: 'marketBased' as any,
auctionStartPriceOffset: 'marketBased' as any,
},
expectedOffset: { from: 'mark', value: 0 },
},
{
name: 'Minor PERP market with market-based params',
input: {
marketIndex: 5,
marketType: 'perp' as any,
direction: 'short' as any,
amount: '500',
assetType: 'quote' as any,
auctionStartPriceOffsetFrom: 'marketBased' as any,
auctionStartPriceOffset: 'marketBased' as any,
},
expectedOffset: { from: 'bestOffer', value: -0.1 },
},
{
name: 'SPOT market with market-based params',
input: {
marketIndex: 2,
marketType: 'spot' as any,
direction: 'long' as any,
amount: '1000',
assetType: 'base' as any,
auctionStartPriceOffsetFrom: 'marketBased' as any,
auctionStartPriceOffset: 'marketBased' as any,
},
expectedOffset: { from: 'bestOffer', value: -0.1 },
},
];
scenarios.forEach(scenario => {
const result = createMarketBasedAuctionParams(scenario.input);
expect(result.auctionStartPriceOffsetFrom).toBe(scenario.expectedOffset.from);
expect(result.auctionStartPriceOffset).toBe(scenario.expectedOffset.value);
// Verify all original parameters are preserved
expect(result.marketIndex).toBe(scenario.input.marketIndex);
expect(result.marketType).toBe(scenario.input.marketType);
expect(result.direction).toBe(scenario.input.direction);
expect(result.amount).toBe(scenario.input.amount);
expect(result.assetType).toBe(scenario.input.assetType);
});
});
it('should handle parameter precedence correctly', () => {
// Test that explicit values always take precedence over market-based logic
const input = {
marketIndex: 0, // Major market - would normally get 'mark' and 0
marketType: 'perp' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
auctionStartPriceOffsetFrom: 'oracle' as any, // explicit value
auctionStartPriceOffset: 0.25, // explicit value
auctionEndPriceOffset: 0.5, // explicit value
auctionDuration: 45, // explicit value
};
const result = createMarketBasedAuctionParams(input);
// Should use explicit values, not market-based defaults
expect(result.auctionStartPriceOffsetFrom).toBe('oracle');
expect(result.auctionStartPriceOffset).toBe(0.25);
expect(result.auctionEndPriceOffset).toBe(0.5);
expect(result.auctionDuration).toBe(45);
});
it('should demonstrate end-to-end parameter formatting', () => {
// Create auction params with market-based logic
const input = {
marketIndex: 1,
marketType: 'perp' as any,
direction: 'long' as any,
amount: '100',
assetType: 'base' as any,
auctionStartPriceOffsetFrom: 'marketBased' as any,
auctionStartPriceOffset: 'marketBased' as any,
auctionDuration: 30,
};
const auctionParams = createMarketBasedAuctionParams(input);
// Simulate the structure that would come from deriveMarketOrderParams
const mockDerivedParams = {
auctionStartPriceOffsetFrom: auctionParams.auctionStartPriceOffsetFrom,
auctionStartPriceOffset: new BN(0), // 0 for major market
auctionEndPriceOffsetFrom: auctionParams.auctionEndPriceOffsetFrom,
auctionEndPriceOffset: new BN(100000), // 0.1 with PRICE_PRECISION
auctionDuration: auctionParams.auctionDuration,
slippageTolerance: 0.05,
additionalEndPriceBuffer: new BN(50000), // 0.05 with PRICE_PRECISION
userOrderId: 123,
isOracleOrder: true,
};
const formattedResponse = formatAuctionParamsForResponse(mockDerivedParams);
// Verify the complete flow
expect(formattedResponse.auctionStartPriceOffsetFrom).toBe('mark'); // Market-based for major market
expect(formattedResponse.auctionStartPriceOffset).toBe('0'); // Market-based for major market
expect(formattedResponse.auctionEndPriceOffset).toBe('100000'); // Formatted from BN
expect(formattedResponse.auctionDuration).toBe(30); // Preserved from input
expect(formattedResponse.slippageTolerance).toBe(0.05); // Preserved as number
expect(formattedResponse.additionalEndPriceBuffer).toBe('50000'); // Formatted from BN
expect(formattedResponse.userOrderId).toBe(123); // Preserved as number
expect(formattedResponse.isOracleOrder).toBe(true); // Preserved as boolean
});
});
});

24
src/utils/types.ts Normal file
View File

@@ -0,0 +1,24 @@
import { AssetType, MarketTypeStr } from "@drift-labs/sdk";
import { TradeOffsetPrice } from "@drift/common";
export type AuctionParamArgs = {
// mandatory args
marketIndex: number;
marketType: MarketTypeStr;
direction: 'long' | 'short';
amount: string;
assetType: AssetType;
// optional settings args
reduceOnly?: boolean;
allowInfSlippage?: boolean;
slippageTolerance?: number;
isOracleOrder?: boolean;
auctionDuration?: number;
auctionStartPriceOffset?: number | 'marketBased';
auctionEndPriceOffset?: number;
auctionStartPriceOffsetFrom?: TradeOffsetPrice | 'marketBased';
auctionEndPriceOffsetFrom?: TradeOffsetPrice;
additionalEndPriceBuffer?: string;
userOrderId?: number;
};

View File

@@ -14,14 +14,26 @@ import {
SpotMarketConfig, SpotMarketConfig,
decodeUser, decodeUser,
isVariant, isVariant,
PositionDirection,
ZERO,
BASE_PRECISION,
PRICE_PRECISION,
calculateEstimatedEntryPriceWithL2,
AssetType,
MainnetSpotMarkets,
DevnetSpotMarkets,
QUOTE_PRECISION,
PERCENTAGE_PRECISION_EXP,
} from '@drift-labs/sdk'; } from '@drift-labs/sdk';
import { RedisClient } from '@drift/common/clients'; import { RedisClient } from '@drift/common/clients';
import { logger } from './logger'; import { logger } from './logger';
import { NextFunction, Request, Response } from 'express'; import { NextFunction, Request, Response } from 'express';
import FEATURE_FLAGS from './featureFlags'; import FEATURE_FLAGS from './featureFlags';
import { Connection } from '@solana/web3.js'; import { Connection } from '@solana/web3.js';
import { wsMarketArgs } from 'src/dlob-subscriber/DLOBSubscriberIO'; import { wsMarketArgs } from 'src/dlob-subscriber/DLOBSubscriberIO';
import { DEFAULT_AUCTION_PARAMS } from './constants';
import { AuctionParamArgs } from './types';
import { COMMON_MATH, ENUM_UTILS } from '@drift/common';
export const GROUPING_OPTIONS = [1, 10, 100, 500, 1000]; export const GROUPING_OPTIONS = [1, 10, 100, 500, 1000];
export const GROUPING_DEPENDENCIES = { export const GROUPING_DEPENDENCIES = {
@@ -632,3 +644,581 @@ export const selectMostRecentBySlot = (
return !mostRecent || current.slot > mostRecent.slot ? current : mostRecent; return !mostRecent || current.slot > mostRecent.slot ? current : mostRecent;
}, null); }, null);
}; };
export function createMarketBasedAuctionParams(
args: AuctionParamArgs,
overrideDefaults?: Partial<AuctionParamArgs>
): AuctionParamArgs {
// Determine if this is a major market (PERP with marketIndex 0, 1, or 2)
const isMajorMarket =
args.marketType?.toLowerCase() === 'perp' &&
[0, 1, 2].includes(args.marketIndex);
// Resolve "marketBased" values and undefined values (both should use market-based logic)
const resolvedAuctionStartPriceOffsetFrom =
args.auctionStartPriceOffsetFrom === 'marketBased' || args.auctionStartPriceOffsetFrom === undefined
? isMajorMarket
? 'mark'
: 'bestOffer'
: args.auctionStartPriceOffsetFrom;
const resolvedAuctionStartPriceOffset =
args.auctionStartPriceOffset === 'marketBased' || args.auctionStartPriceOffset === undefined
? isMajorMarket
? 0
: -0.1
: args.auctionStartPriceOffset;
// Set market-specific defaults (only used if values are undefined)
const marketSpecificDefaults: Partial<AuctionParamArgs> = {
...DEFAULT_AUCTION_PARAMS,
auctionStartPriceOffsetFrom: isMajorMarket ? 'mark' : 'bestOffer',
auctionStartPriceOffset: isMajorMarket ? 0 : -0.1,
};
// Apply custom overrides if provided
const finalDefaults = overrideDefaults
? { ...marketSpecificDefaults, ...overrideDefaults }
: marketSpecificDefaults;
return {
...finalDefaults,
...args,
// Override with resolved "marketBased" values if were provided
auctionStartPriceOffsetFrom:
resolvedAuctionStartPriceOffsetFrom ??
finalDefaults.auctionStartPriceOffsetFrom,
auctionStartPriceOffset:
resolvedAuctionStartPriceOffset ?? finalDefaults.auctionStartPriceOffset,
};
}
/**
* Parse boolean values from string query parameters
* @param value - string value from query parameter
* @returns boolean | undefined - true for 'true'/'1', false for other values, undefined if input is undefined
*/
export const parseBoolean = (
value: string | undefined
): boolean | undefined => {
if (value === undefined) return undefined;
return value === 'true' || value === '1';
};
/**
* Safely parse numeric values from string query parameters
* @param value - string value from query parameter
* @returns number | undefined - parsed number or undefined if invalid/empty
*/
export const parseNumber = (value: string | undefined): number | undefined => {
if (!value) return undefined;
const parsed = parseFloat(value);
return isNaN(parsed) ? undefined : parsed;
};
/**
* Convert string to BN
* @param value - string value to convert
* @returns BN
*/
export const stringToBN = (value: string): BN => {
if (!value) return ZERO;
return new BN(value);
};
/**
* Convert raw Redis L2 data (with string prices/sizes) to proper L2OrderBook format (with BN values)
* @param rawL2 - Raw L2 data from Redis with string values
* @returns L2OrderBook with proper BN values
*/
export const convertRawL2ToBN = (rawL2: any): L2OrderBook => {
const convertLevel = (level: any) => ({
...level,
price: new BN(level.price),
size: new BN(level.size),
});
return {
...rawL2,
bids: rawL2.bids?.map(convertLevel) || [],
asks: rawL2.asks?.map(convertLevel) || [],
};
};
/**
* Get L2 orderbook data and calculate estimated prices
* @param driftClient - DriftClient instance
* @param marketType - MarketType enum
* @param marketIndex - Market index number
* @param direction - Position direction
* @param amount - Amount as BN (could be base or quote amount)
* @param assetType - Whether amount is 'base' or 'quote'
* @param fetchFromRedis - Redis fetch function
* @param selectMostRecentBySlot - Slot selection function
* @returns Price data object with oracle, best, entry, worst, and mark prices
*/
export const getEstimatedPrices = async (
driftClient: DriftClient,
marketType: MarketType,
marketIndex: number,
direction: PositionDirection,
amount: BN,
assetType: AssetType,
fetchFromRedis: (
key: string,
selectionCriteria: (responses: any) => any
) => Promise<any>,
selectMostRecentBySlot: (responses: any[]) => any
): Promise<{
oraclePrice: BN;
bestPrice: BN;
entryPrice: BN;
worstPrice: BN;
markPrice: BN;
priceImpact: BN;
}> => {
const isSpot = isVariant(marketType, 'spot');
// Get L2 orderbook data using the new utility function
const redisL2 = await fetchL2FromRedis(
fetchFromRedis,
selectMostRecentBySlot,
marketType,
marketIndex
);
let l2Formatted: L2OrderBook;
if (redisL2) {
l2Formatted = convertRawL2ToBN(redisL2);
} else {
l2Formatted = {
bids: [],
asks: [],
};
}
const oracleData = isSpot
? driftClient.getOracleDataForSpotMarket(marketIndex)
: driftClient.getOracleDataForPerpMarket(marketIndex);
// Get oracle price
const oraclePrice = new BN(oracleData?.price || 0).mul(PRICE_PRECISION);
const spreadInfo = COMMON_MATH.calculateSpreadBidAskMark(
l2Formatted,
oraclePrice
);
const markPrice = spreadInfo?.markPrice ?? oraclePrice;
// If we have L2 data, calculate estimated prices
if (l2Formatted.bids?.length > 0 || l2Formatted.asks?.length > 0) {
try {
const basePrecision = !isSpot
? BASE_PRECISION
: process.env.ENV === 'mainnet-beta'
? MainnetSpotMarkets[marketIndex].precision
: DevnetSpotMarkets[marketIndex].precision;
const priceEstimate = calculateEstimatedEntryPriceWithL2(
assetType,
amount,
direction,
basePrecision,
l2Formatted as L2OrderBook
);
return {
oraclePrice,
bestPrice: priceEstimate.bestPrice,
entryPrice: priceEstimate.entryPrice,
worstPrice: priceEstimate.worstPrice,
markPrice,
priceImpact: priceEstimate.priceImpact,
};
} catch (error) {
// If calculation fails, fallback to oracle prices
console.warn('Price calculation failed, using oracle fallback:', error);
}
}
// Fallback to oracle prices if no L2 data or calculation fails
return {
oraclePrice,
bestPrice: oraclePrice,
entryPrice: oraclePrice,
worstPrice: oraclePrice,
markPrice,
priceImpact: ZERO,
};
};
/**
* Maps AuctionParamArgs to the format expected by deriveMarketOrderParams
* @param params - AuctionParamArgs from the API request
* @param driftClient - DriftClient instance (optional, for price calculation)
* @param fetchFromRedis - Redis fetch function (optional, for price calculation)
* @param selectMostRecentBySlot - Slot selection function (optional, for price calculation)
* @returns Object formatted for deriveMarketOrderParams function or error response
*/
export const mapToMarketOrderParams = async (
params: AuctionParamArgs,
driftClient?: DriftClient,
fetchFromRedis?: (
key: string,
selectionCriteria: (responses: any) => any
) => Promise<any>,
selectMostRecentBySlot?: (responses: any[]) => any
): Promise<{
success: boolean;
data?: {
marketOrderParams: any;
estimatedPrices: any;
};
error?: string;
}> => {
// Convert marketType string to MarketType enum
const marketType =
params.marketType.toLowerCase() === 'spot'
? MarketType.SPOT
: MarketType.PERP;
// Convert direction string to PositionDirection enum
const direction =
params.direction === 'long'
? PositionDirection.LONG
: PositionDirection.SHORT;
// Convert amount string to BN based on assetType
const amount =
params.assetType === 'base'
? stringToBN(params.amount).mul(BASE_PRECISION)
: stringToBN(params.amount).mul(QUOTE_PRECISION);
// Convert additionalEndPriceBuffer string to BN with PRICE_PRECISION (1e6) if provided
const additionalEndPriceBuffer = params.additionalEndPriceBuffer
? stringToBN(params.additionalEndPriceBuffer).mul(PRICE_PRECISION)
: undefined;
// Calculate estimated prices and handle slippage tolerance calculation
let estimatedPrices;
let processedSlippageTolerance = params.slippageTolerance;
if (driftClient && fetchFromRedis && selectMostRecentBySlot) {
// Get L2 orderbook data using the utility function
const redisL2 = await fetchL2FromRedis(
fetchFromRedis,
selectMostRecentBySlot,
marketType,
params.marketIndex
);
// Calculate estimated prices using the fetched L2 data
estimatedPrices = await getEstimatedPricesWithL2(
driftClient,
marketType,
params.marketIndex,
direction,
amount,
params.assetType,
redisL2
);
// Handle dynamic slippage tolerance calculation if needed
if (params.slippageTolerance === undefined) {
// Convert raw L2 to formatted L2 for slippage calculation
let l2Formatted: L2OrderBook;
if (redisL2) {
l2Formatted = convertRawL2ToBN(redisL2);
} else {
l2Formatted = {
bids: [],
asks: [],
};
}
processedSlippageTolerance = calculateDynamicSlippage(
params.marketIndex,
params.marketType,
driftClient,
l2Formatted
);
}
} else {
return {
success: false,
error: 'Cannot create valid auction parameters: could not fetch prices',
};
}
// Calculate baseAmount based on assetType
let baseAmount: BN;
if (params.assetType === 'base') {
// If assetType is base, use the amount directly
baseAmount = amount;
} else {
// If assetType is quote, convert quote amount to base amount using entry price
// baseAmount = (quoteAmount * QUOTE_PRECISION * BASE_PRECISION) / entryPrice
baseAmount = amount.mul(BASE_PRECISION).div(estimatedPrices.entryPrice);
}
return {
success: true,
data: {
marketOrderParams: {
marketType,
marketIndex: params.marketIndex,
direction,
maxLeverageSelected: false,
maxLeverageOrderSize: ZERO,
baseAmount,
reduceOnly: params.reduceOnly ?? false,
allowInfSlippage: params.allowInfSlippage ?? false,
oraclePrice: estimatedPrices.oraclePrice,
bestPrice: estimatedPrices.bestPrice,
entryPrice: estimatedPrices.entryPrice,
worstPrice: estimatedPrices.worstPrice,
markPrice: estimatedPrices.markPrice,
auctionDuration: params.auctionDuration,
auctionStartPriceOffset: params.auctionStartPriceOffset as number,
auctionEndPriceOffset: params.auctionEndPriceOffset,
auctionStartPriceOffsetFrom: params.auctionStartPriceOffsetFrom as any,
auctionEndPriceOffsetFrom: params.auctionEndPriceOffsetFrom,
slippageTolerance: processedSlippageTolerance,
isOracleOrder: params.isOracleOrder,
additionalEndPriceBuffer,
forceUpToSlippage: true,
userOrderId: params.userOrderId,
},
estimatedPrices,
},
};
};
/**
* Format auction parameters for API response
* @param auctionParams - Raw auction parameters from deriveMarketOrderParams
* @returns Formatted auction parameters with BNs as strings and enums as readable strings
*/
export const formatAuctionParamsForResponse = (auctionParams: any) => {
const formatted = { ...auctionParams };
// we don't use this field anymore, TODO to remove from ui
delete formatted.constrainedBySlippage;
// Convert all properties
Object.keys(formatted).forEach((key) => {
const value = formatted[key];
// Check if it's a BN using BN.isBN()
if (BN.isBN(value)) {
formatted[key] = value.toString();
}
// Check if it's an enum (has nested object structure like {oracle: {}})
else if (
value &&
typeof value === 'object' &&
Object.keys(value).length === 1
) {
try {
formatted[key] = ENUM_UTILS.toStr(value);
} catch (e) {
// If ENUM_UTILS.toStr fails, keep original value
formatted[key] = value;
}
}
});
return formatted;
};
/**
* Fetch L2 orderbook data from Redis
* @param fetchFromRedis - Redis fetch function
* @param selectMostRecentBySlot - Slot selection function
* @param marketType - MarketType enum (spot or perp)
* @param marketIndex - Market index number
* @param includeIndicative - Whether to include indicative orders (optional)
* @returns Promise<any> - Raw L2 data from Redis or null if not found
*/
export const fetchL2FromRedis = async (
fetchFromRedis: (
key: string,
selectionCriteria: (responses: any) => any
) => Promise<any>,
selectMostRecentBySlot: (responses: any[]) => any,
marketType: MarketType,
marketIndex: number,
includeIndicative?: boolean
): Promise<any> => {
const isSpot = isVariant(marketType, 'spot');
const marketTypeStr = isSpot ? 'spot' : 'perp';
const indicativeSuffix = includeIndicative ? '_indicative' : '';
return await fetchFromRedis(
`last_update_orderbook_${marketTypeStr}_${marketIndex}${indicativeSuffix}`,
selectMostRecentBySlot
);
};
/**
* Calculate dynamic slippage tolerance using L2 data
* @param direction - Position direction ('long' or 'short')
* @param marketIndex - Market index number
* @param marketType - Market type ('spot' or 'perp')
* @param driftClient - DriftClient instance for oracle data
* @param l2Formatted - Already formatted L2OrderBook data
* @returns Dynamic slippage tolerance as a number
*/
export const calculateDynamicSlippage = (
marketIndex: number,
marketType: string,
driftClient: DriftClient,
l2Formatted: L2OrderBook
): number => {
// Determine if this is a major market (PERP with marketIndex 0, 1, or 2)
const isPerp = marketType.toLowerCase() === 'perp';
const isMajor = isPerp && marketIndex < 3;
const baseSlippage = isMajor
? parseFloat(process.env.DYNAMIC_BASE_SLIPPAGE_MAJOR || '0') // 0% default
: parseFloat(process.env.DYNAMIC_BASE_SLIPPAGE_NON_MAJOR || '0.5'); // 0.5% default
// Calculate spread using L2 data
let spreadBaseSlippage = 0.0005; // 0.05% fallback spread
try {
// Get oracle data
const oracleData = isPerp
? driftClient.getOracleDataForPerpMarket(marketIndex)
: driftClient.getOracleDataForSpotMarket(marketIndex);
// Get oracle price
const oraclePrice = new BN(oracleData?.price || 0).mul(PRICE_PRECISION);
// Calculate actual spread
const spreadInfo = COMMON_MATH.calculateSpreadBidAskMark(
l2Formatted,
oraclePrice
);
const spreadPctNum = BigNum.from(
spreadInfo.spreadPct,
PERCENTAGE_PRECISION_EXP
)?.toNum();
if (spreadInfo?.spreadPct) {
spreadBaseSlippage = spreadPctNum / 2;
}
} catch (error) {
console.warn('Failed to calculate spread, using fallback:', error);
}
let dynamicSlippage = baseSlippage + spreadBaseSlippage;
// Apply multiplier from env var
const multiplier = isMajor
? parseFloat(process.env.DYNAMIC_SLIPPAGE_MULTIPLIER_MAJOR || '1.02')
: parseFloat(process.env.DYNAMIC_SLIPPAGE_MULTIPLIER_NON_MAJOR || '1.5');
dynamicSlippage = dynamicSlippage * multiplier;
// Enforce minimum and maximum limits from env vars
const minSlippage = parseFloat(process.env.DYNAMIC_SLIPPAGE_MIN || '0.05'); // 0.05% minimum
const maxSlippage = parseFloat(process.env.DYNAMIC_SLIPPAGE_MAX || '5'); // 5% maximum
return Math.min(Math.max(dynamicSlippage, minSlippage), maxSlippage);
};
/**
* Get L2 orderbook data and calculate estimated prices using pre-fetched L2 data
* @param driftClient - DriftClient instance
* @param marketType - MarketType enum
* @param marketIndex - Market index number
* @param direction - Position direction
* @param amount - Amount as BN (could be base or quote amount)
* @param assetType - Whether amount is 'base' or 'quote'
* @param redisL2 - Pre-fetched L2 data from Redis
* @returns Price data object with oracle, best, entry, worst, and mark prices
*/
export const getEstimatedPricesWithL2 = async (
driftClient: DriftClient,
marketType: MarketType,
marketIndex: number,
direction: PositionDirection,
amount: BN,
assetType: AssetType,
redisL2: any
): Promise<{
oraclePrice: BN;
bestPrice: BN;
entryPrice: BN;
worstPrice: BN;
markPrice: BN;
priceImpact: BN;
}> => {
const isSpot = isVariant(marketType, 'spot');
let l2Formatted: L2OrderBook;
if (redisL2) {
l2Formatted = convertRawL2ToBN(redisL2);
} else {
l2Formatted = {
bids: [],
asks: [],
};
}
const oracleData = isSpot
? driftClient.getOracleDataForSpotMarket(marketIndex)
: driftClient.getOracleDataForPerpMarket(marketIndex);
// Get oracle price
const oraclePrice = oracleData.price ?? ZERO;
const spreadInfo = COMMON_MATH.calculateSpreadBidAskMark(
l2Formatted,
oraclePrice
);
const markPrice = spreadInfo?.markPrice ?? oraclePrice;
// If we have L2 data, calculate estimated prices
if (l2Formatted.bids?.length > 0 || l2Formatted.asks?.length > 0) {
try {
const basePrecision = !isSpot
? BASE_PRECISION
: process.env.ENV === 'mainnet-beta'
? MainnetSpotMarkets[marketIndex].precision
: DevnetSpotMarkets[marketIndex].precision;
const priceEstimate = calculateEstimatedEntryPriceWithL2(
assetType,
amount,
direction,
basePrecision,
l2Formatted as L2OrderBook
);
return {
oraclePrice,
bestPrice: priceEstimate.bestPrice,
entryPrice: priceEstimate.entryPrice,
worstPrice: priceEstimate.worstPrice,
markPrice,
priceImpact: priceEstimate.priceImpact,
};
} catch (error) {
// If calculation fails, fallback to oracle prices
console.warn('Price calculation failed, using oracle fallback:', error);
}
}
// Fallback to oracle prices if no L2 data or calculation fails
return {
oraclePrice,
bestPrice: oraclePrice,
entryPrice: oraclePrice,
worstPrice: oraclePrice,
markPrice,
priceImpact: ZERO,
};
};

1077
yarn.lock

File diff suppressed because it is too large Load Diff