From 0ead3a63a81e9789ae5d16a06b929a31a5e56507 Mon Sep 17 00:00:00 2001 From: wphan Date: Wed, 3 Jan 2024 11:18:58 -0800 Subject: [PATCH] liquidator: use limit orders with auction params (#119) * liquidator: use limit orders with auction params * liquidator: normalize slippage config to bps * fix priority fee rounding --- example.config.yaml | 12 +++++-- src/bots/liquidator.ts | 71 ++++++++++++++++++++++++++++++++++++------ src/config.ts | 10 +++++- 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/example.config.yaml b/example.config.yaml index ff4165b2..722d79c2 100644 --- a/example.config.yaml +++ b/example.config.yaml @@ -114,8 +114,16 @@ botConfigs: spotSubAccountConfig: 0: # subaccount 0 will watch all spot markets - # max slippage (from oracle price) to incur allow when derisking - maxSlippagePct: 0.005 + # deprecated (bad naming): use maxSlippageBps + maxSlippagePct: 50 + + # Max slippage to incur allow when derisking (in bps). + # This is used to calculate the auction end price (worse price) when derisking + # and also passed into jupiter when doing swaps. + maxSlippageBps: 50 + + # duration of jit auction for derisk orders + deriskAuctionDurationSlots: 100 # what algo to use for derisking. Options are "market" or "twap" deriskAlgo: "market" diff --git a/src/bots/liquidator.ts b/src/bots/liquidator.ts index 9a7f426d..4a615793 100644 --- a/src/bots/liquidator.ts +++ b/src/bots/liquidator.ts @@ -20,7 +20,6 @@ import { SerumFulfillmentConfigMap, initialize, DriftEnv, - getMarketOrderParams, findDirectionToClose, getSignedTokenAmount, standardizeBaseAssetAmount, @@ -38,6 +37,8 @@ import { TxParams, PriorityFeeSubscriber, QuoteResponse, + getLimitOrderParams, + PERCENTAGE_PRECISION, } from '@drift-labs/sdk'; import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'; @@ -76,6 +77,7 @@ const errorCodesToSuppress = [ 6010, // Error Number: 6010. Error Message: User Has No Position In Market. ]; +const BPS_PRECISION = 10000; const LIQUIDATE_THROTTLE_BACKOFF = 5000; // the time to wait before trying to liquidate a throttled user again const MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS = 20_000; // cap the computeUnitPrice to pay per fill tx @@ -326,6 +328,10 @@ export class LiquidatorBot implements Bot { SERUM_LOOKUP_TABLE: PublicKey ) { this.liquidatorConfig = config; + if (this.liquidatorConfig.maxSlippageBps === undefined) { + this.liquidatorConfig.maxSlippageBps = + this.liquidatorConfig.maxSlippagePct!; + } this.liquidatorConfig.deriskAlgoPerp = config.deriskAlgoPerp ?? config.deriskAlgo; @@ -534,7 +540,7 @@ export class LiquidatorBot implements Bot { return { computeUnits: 1_400_000, computeUnitsPrice: Math.min( - this.priorityFeeSubscriber.maxPriorityFee * 1.1, + Math.floor(this.priorityFeeSubscriber.maxPriorityFee), MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS ), }; @@ -653,16 +659,48 @@ export class LiquidatorBot implements Bot { return healthy; } + /** + * Calculates the worse price to execute at (used for auction end when derisking) + * @param oracle for asset we trading + * @param direction of trade + * @returns + */ private calculateOrderLimitPrice( oracle: OraclePriceData, direction: PositionDirection ): BN { - const slippageBN = new BN(this.liquidatorConfig.maxSlippagePct! * 10000); + const slippageBN = new BN( + (this.liquidatorConfig.maxSlippageBps! / BPS_PRECISION) * + PERCENTAGE_PRECISION.toNumber() + ); + if (isVariant(direction, 'long')) { + return oracle.price + .mul(PERCENTAGE_PRECISION.add(slippageBN)) + .div(PERCENTAGE_PRECISION); + } else { + return oracle.price + .mul(PERCENTAGE_PRECISION.sub(slippageBN)) + .div(PERCENTAGE_PRECISION); + } + } + + /** + * Calcualtes the auctionStart price when derisking (the best price we want to execute at) + * @param oracle for asset we trading + * @param direction of trade + * @returns + */ + private calculateDeriskAuctionStartPrice( + oracle: OraclePriceData, + direction: PositionDirection + ): BN { + let auctionStartPrice: BN; if (isVariant(direction, 'long')) { - return oracle.price.mul(new BN(10000).add(slippageBN)).div(new BN(10000)); + auctionStartPrice = oracle.price.sub(oracle.confidence); } else { - return oracle.price.mul(new BN(10000).sub(slippageBN)).div(new BN(10000)); + auctionStartPrice = oracle.price.add(oracle.confidence); } + return auctionStartPrice; } private async driftSpotTrade( @@ -696,13 +734,22 @@ export class LiquidatorBot implements Bot { return; } + const oracle = this.driftClient.getOracleDataForSpotMarket(marketIndex); + const auctionStartPrice = this.calculateDeriskAuctionStartPrice( + oracle, + orderDirection + ); + const tx = await this.driftClient.placeSpotOrder( - getMarketOrderParams({ + getLimitOrderParams({ marketIndex: marketIndex, direction: orderDirection, baseAssetAmount: standardizedTokenAmount, reduceOnly: true, price: limitPrice, + auctionDuration: this.liquidatorConfig.deriskAuctionDurationSlots!, + auctionStartPrice, + auctionEndPrice: limitPrice, }) ); logger.info( @@ -892,13 +939,20 @@ export class LiquidatorBot implements Bot { ); const direction = findDirectionToClose(position); const limitPrice = this.calculateOrderLimitPrice(oracle, direction); + const auctionStartPrice = this.calculateDeriskAuctionStartPrice( + oracle, + direction + ); - return getMarketOrderParams({ + return getLimitOrderParams({ direction, baseAssetAmount, reduceOnly: true, marketIndex: position.marketIndex, price: limitPrice, + auctionDuration: this.liquidatorConfig.deriskAuctionDurationSlots!, + auctionEndPrice: limitPrice, + auctionStartPrice, }); } @@ -1245,8 +1299,7 @@ export class LiquidatorBot implements Bot { continue; } - const slippageDenom = 10000; - const slippageBps = this.liquidatorConfig.maxSlippagePct! * slippageDenom; + const slippageBps = this.liquidatorConfig.maxSlippageBps! * BPS_PRECISION; const jupQuote = await this.determineBestSpotSwapRoute( position.marketIndex, orderParams.direction, diff --git a/src/config.ts b/src/config.ts index 9a9f78a2..1740802f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -36,7 +36,12 @@ export type LiquidatorConfig = BaseBotConfig & { spotMarketIndicies?: Array; perpSubAccountConfig?: SubaccountConfig; spotSubAccountConfig?: SubaccountConfig; + + // deprecated: use maxSlippageBps (misnamed) maxSlippagePct?: number; + maxSlippageBps?: number; + + deriskAuctionDurationSlots?: number; deriskAlgo?: OrderExecutionAlgoType; deriskAlgoSpot?: OrderExecutionAlgoType; deriskAlgoPerp?: OrderExecutionAlgoType; @@ -251,7 +256,10 @@ export function loadConfigFromOpts(opts: any): Config { perpMarketIndicies: loadCommaDelimitToArray(opts.perpMarketIndicies), spotMarketIndicies: loadCommaDelimitToArray(opts.spotMarketIndicies), runOnce: opts.runOnce ?? false, - maxSlippagePct: opts.maxSlippagePct ?? 0.05, + // deprecated: use maxSlippageBps + maxSlippagePct: opts.maxSlippagePct ?? 50, + maxSlippageBps: opts.maxSlippageBps ?? 50, + deriskAuctionDurationSlots: opts.deriskAuctionDurationSlots ?? 100, deriskAlgo: opts.deriskAlgo ?? OrderExecutionAlgoType.Market, twapDurationSec: parseInt(opts.twapDurationSec ?? '300'), notifyOnLiquidation: opts.notifyOnLiquidation ?? false,