/** * Drift Order Execution * * Handles opening and closing positions with market orders */ import { getDriftService, initializeDriftService } from './client' import { getMarketConfig } from '../../config/trading' import BN from 'bn.js' import { MarketType, PositionDirection, OrderType, OrderParams, OrderTriggerCondition, } from '@drift-labs/sdk' export interface OpenPositionParams { symbol: string // e.g., 'SOL-PERP' direction: 'long' | 'short' sizeUSD: number // USD notional size slippageTolerance: number // Percentage (e.g., 1.0 for 1%) } export interface OpenPositionResult { success: boolean transactionSignature?: string fillPrice?: number fillSize?: number slippage?: number error?: string isPhantom?: boolean // Position opened but size mismatch detected actualSizeUSD?: number // Actual position size if different from requested } export interface ClosePositionParams { symbol: string percentToClose: number // 0-100 slippageTolerance: number } export interface ClosePositionResult { success: boolean transactionSignature?: string closePrice?: number closedSize?: number realizedPnL?: number needsVerification?: boolean error?: string } export interface PlaceExitOrdersResult { success: boolean signatures?: string[] error?: string } export interface PlaceExitOrdersOptions { symbol: string positionSizeUSD: number entryPrice: number // CRITICAL: Entry price for calculating position size in base assets tp1Price: number tp2Price: number stopLossPrice: number tp1SizePercent: number tp2SizePercent: number direction: 'long' | 'short' useStopLimit?: boolean // Optional: use TRIGGER_LIMIT instead of TRIGGER_MARKET for SL stopLimitBuffer?: number // Optional: buffer percentage for stop-limit (default 0.5%) // Dual Stop System useDualStops?: boolean // Enable dual stop system softStopPrice?: number // Soft stop trigger price (TRIGGER_LIMIT) softStopBuffer?: number // Buffer for soft stop limit price hardStopPrice?: number // Hard stop trigger price (TRIGGER_MARKET) } /** * Open a position with a market order */ export async function openPosition( params: OpenPositionParams ): Promise { try { console.log('๐Ÿ“Š Opening position:', params) const driftService = getDriftService() const marketConfig = getMarketConfig(params.symbol) const driftClient = driftService.getClient() // Get current oracle price const oraclePrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex) console.log(`๐Ÿ’ฐ Current ${params.symbol} price: $${oraclePrice.toFixed(4)}`) // Calculate position size in base asset const baseAssetSize = params.sizeUSD / oraclePrice // Validate minimum order size if (baseAssetSize < marketConfig.minOrderSize) { throw new Error( `Order size ${baseAssetSize.toFixed(4)} is below minimum ${marketConfig.minOrderSize}` ) } // Calculate worst acceptable price (with slippage) const slippageMultiplier = params.direction === 'long' ? 1 + (params.slippageTolerance / 100) : 1 - (params.slippageTolerance / 100) const worstPrice = oraclePrice * slippageMultiplier console.log(`๐Ÿ“ Order details:`) console.log(` Size: ${baseAssetSize.toFixed(4)} ${params.symbol.split('-')[0]}`) console.log(` Notional: $${params.sizeUSD.toFixed(2)}`) console.log(` Oracle price: $${oraclePrice.toFixed(4)}`) console.log(` Worst price (${params.slippageTolerance}% slippage): $${worstPrice.toFixed(4)}`) // Check DRY_RUN mode const isDryRun = process.env.DRY_RUN === 'true' if (isDryRun) { console.log('๐Ÿงช DRY RUN MODE: Simulating order (not executing on blockchain)') const mockTxSig = `DRY_RUN_${Date.now()}_${Math.random().toString(36).substring(7)}` return { success: true, transactionSignature: mockTxSig, fillPrice: oraclePrice, fillSize: baseAssetSize, slippage: 0, } } // Prepare order parameters - use simple structure like v3 const orderParams = { orderType: OrderType.MARKET, marketIndex: marketConfig.driftMarketIndex, direction: params.direction === 'long' ? PositionDirection.LONG : PositionDirection.SHORT, baseAssetAmount: new BN(Math.floor(baseAssetSize * 1e9)), // 9 decimals reduceOnly: false, } // Place market order using simple placePerpOrder (like v3) console.log('๐Ÿš€ Placing REAL market order...') const txSig = await driftClient.placePerpOrder(orderParams) console.log(`๐Ÿ“ Transaction submitted: ${txSig}`) // CRITICAL: Confirm transaction actually executed on-chain console.log('โณ Confirming transaction on-chain...') const connection = driftService.getTradeConnection() // Use Alchemy for trade operations try { const confirmation = await connection.confirmTransaction(txSig, 'confirmed') if (confirmation.value.err) { console.error(`โŒ Transaction failed on-chain:`, confirmation.value.err) return { success: false, error: `Transaction failed: ${JSON.stringify(confirmation.value.err)}`, } } console.log(`โœ… Transaction confirmed on-chain: ${txSig}`) } catch (confirmError) { console.error(`โŒ Failed to confirm transaction:`, confirmError) return { success: false, error: `Transaction confirmation failed: ${confirmError instanceof Error ? confirmError.message : 'Unknown error'}`, } } // Wait a moment for position to update console.log('โณ Waiting for position to update...') await new Promise(resolve => setTimeout(resolve, 2000)) // Get actual fill price from position const position = await driftService.getPosition(marketConfig.driftMarketIndex) if (position && position.side !== 'none') { const fillPrice = position.entryPrice const slippage = Math.abs((fillPrice - oraclePrice) / oraclePrice) * 100 // CRITICAL: Validate actual position size vs expected // Phantom trade detection: Check if position is significantly smaller than expected const actualSizeUSD = position.size * fillPrice const expectedSizeUSD = params.sizeUSD const sizeRatio = actualSizeUSD / expectedSizeUSD console.log(`๐Ÿ’ฐ Fill details:`) console.log(` Fill price: $${fillPrice.toFixed(4)}`) console.log(` Slippage: ${slippage.toFixed(3)}%`) console.log(` Expected size: $${expectedSizeUSD.toFixed(2)}`) console.log(` Actual size: $${actualSizeUSD.toFixed(2)}`) console.log(` Size ratio: ${(sizeRatio * 100).toFixed(1)}%`) // Flag as phantom if actual size is less than 50% of expected const isPhantom = sizeRatio < 0.5 if (isPhantom) { console.error(`๐Ÿšจ PHANTOM POSITION DETECTED!`) console.error(` Expected: $${expectedSizeUSD.toFixed(2)}`) console.error(` Actual: $${actualSizeUSD.toFixed(2)}`) console.error(` This indicates the order was rejected or partially filled by Drift`) } return { success: true, transactionSignature: txSig, fillPrice, fillSize: position.size, // Use actual size from Drift, not calculated slippage, isPhantom, actualSizeUSD, } } else { // Position not found yet (may be DRY_RUN mode) console.log(`โš ๏ธ Position not immediately visible (may be DRY_RUN mode)`) console.log(` Using oracle price as estimate: $${oraclePrice.toFixed(4)}`) return { success: true, transactionSignature: txSig, fillPrice: oraclePrice, fillSize: baseAssetSize, slippage: 0, } } } catch (error) { console.error('โŒ Failed to open position:', error) return { success: false, error: error instanceof Error ? error.message : 'Unknown error', } } } /** * Place on-chain exit orders (reduce-only orders) so TP/SL show up in Drift UI. * * Stop Loss Strategy: * - Default: TRIGGER_MARKET (guaranteed execution, recommended for most traders) * - Optional: TRIGGER_LIMIT with buffer (protects against extreme wicks in liquid markets) * * Take Profit Strategy: * - Always uses LIMIT orders to lock in desired prices */ export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise { try { console.log('๐Ÿ›ก๏ธ Placing exit orders on-chain:', options.symbol) const driftService = getDriftService() const driftClient = driftService.getClient() const marketConfig = getMarketConfig(options.symbol) const isDryRun = process.env.DRY_RUN === 'true' if (isDryRun) { console.log('๐Ÿงช DRY RUN: Simulating placement of exit orders') return { success: true, signatures: [ `DRY_TP1_${Date.now()}`, `DRY_TP2_${Date.now()}`, `DRY_SL_${Date.now()}`, ], } } const signatures: string[] = [] // Helper to compute base asset amount from USD notional and price // CRITICAL: Use ENTRY price to calculate position size, not TP price! // This ensures we close the correct percentage of the actual position const usdToBase = (usd: number) => { const base = usd / options.entryPrice // Use entry price for size calculation return Math.floor(base * 1e9) // 9 decimals expected by SDK } // Calculate sizes in USD for each TP // CRITICAL FIX: TP2 must be percentage of REMAINING size after TP1, not original size const tp1USD = (options.positionSizeUSD * options.tp1SizePercent) / 100 const remainingAfterTP1 = options.positionSizeUSD - tp1USD const tp2USD = (remainingAfterTP1 * options.tp2SizePercent) / 100 console.log(`๐Ÿ“Š Exit order sizes:`) console.log(` TP1: ${options.tp1SizePercent}% of $${options.positionSizeUSD.toFixed(2)} = $${tp1USD.toFixed(2)}`) console.log(` Remaining after TP1: $${remainingAfterTP1.toFixed(2)}`) console.log(` TP2: ${options.tp2SizePercent}% of remaining = $${tp2USD.toFixed(2)}`) console.log(` Runner (if any): $${(remainingAfterTP1 - tp2USD).toFixed(2)}`) // For orders that close a long, the order direction should be SHORT (sell) const orderDirection = options.direction === 'long' ? PositionDirection.SHORT : PositionDirection.LONG // Place TP1 LIMIT reduce-only if (tp1USD > 0) { const baseAmount = usdToBase(tp1USD) if (baseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) { const orderParams: any = { orderType: OrderType.LIMIT, marketIndex: marketConfig.driftMarketIndex, direction: orderDirection, baseAssetAmount: new BN(baseAmount), price: new BN(Math.floor(options.tp1Price * 1e6)), // price in 1e6 reduceOnly: true, } console.log('๐Ÿšง Placing TP1 limit order (reduce-only)...') const sig = await retryWithBackoff(async () => await (driftClient as any).placePerpOrder(orderParams) ) console.log('โœ… TP1 order placed:', sig) signatures.push(sig) } else { console.log('โš ๏ธ TP1 size below market min, skipping on-chain TP1') } } // Place TP2 LIMIT reduce-only if (tp2USD > 0) { const baseAmount = usdToBase(tp2USD) if (baseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) { const orderParams: any = { orderType: OrderType.LIMIT, marketIndex: marketConfig.driftMarketIndex, direction: orderDirection, baseAssetAmount: new BN(baseAmount), price: new BN(Math.floor(options.tp2Price * 1e6)), reduceOnly: true, } console.log('๐Ÿšง Placing TP2 limit order (reduce-only)...') const sig = await retryWithBackoff(async () => await (driftClient as any).placePerpOrder(orderParams) ) console.log('โœ… TP2 order placed:', sig) signatures.push(sig) } else { console.log('โš ๏ธ TP2 size below market min, skipping on-chain TP2') } } // Place Stop-Loss order(s) // Supports three modes: // 1. Dual Stop System (soft stop-limit + hard stop-market) // 2. Single TRIGGER_LIMIT (for liquid markets) // 3. Single TRIGGER_MARKET (default, guaranteed execution) const slUSD = options.positionSizeUSD const slBaseAmount = usdToBase(slUSD) if (slBaseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) { const useDualStops = options.useDualStops ?? false if (useDualStops && options.softStopPrice && options.hardStopPrice) { // ============== DUAL STOP SYSTEM ============== console.log('๐Ÿ›ก๏ธ๐Ÿ›ก๏ธ Placing DUAL STOP SYSTEM...') // 1. Soft Stop (TRIGGER_LIMIT) - Avoids wicks const softStopBuffer = options.softStopBuffer ?? 0.4 const softStopMultiplier = options.direction === 'long' ? (1 - softStopBuffer / 100) : (1 + softStopBuffer / 100) const softStopParams: any = { orderType: OrderType.TRIGGER_LIMIT, marketIndex: marketConfig.driftMarketIndex, direction: orderDirection, baseAssetAmount: new BN(slBaseAmount), triggerPrice: new BN(Math.floor(options.softStopPrice * 1e6)), price: new BN(Math.floor(options.softStopPrice * softStopMultiplier * 1e6)), triggerCondition: options.direction === 'long' ? OrderTriggerCondition.BELOW : OrderTriggerCondition.ABOVE, reduceOnly: true, } console.log(` 1๏ธโƒฃ Soft Stop (TRIGGER_LIMIT):`) console.log(` Trigger: $${options.softStopPrice.toFixed(4)}`) console.log(` Limit: $${(options.softStopPrice * softStopMultiplier).toFixed(4)}`) console.log(` Purpose: Avoid false breakouts/wicks`) const softStopSig = await retryWithBackoff(async () => await (driftClient as any).placePerpOrder(softStopParams) ) console.log(` โœ… Soft stop placed: ${softStopSig}`) signatures.push(softStopSig) // 2. Hard Stop (TRIGGER_MARKET) - Guarantees exit const hardStopParams: any = { orderType: OrderType.TRIGGER_MARKET, marketIndex: marketConfig.driftMarketIndex, direction: orderDirection, baseAssetAmount: new BN(slBaseAmount), triggerPrice: new BN(Math.floor(options.hardStopPrice * 1e6)), triggerCondition: options.direction === 'long' ? OrderTriggerCondition.BELOW : OrderTriggerCondition.ABOVE, reduceOnly: true, } console.log(` 2๏ธโƒฃ Hard Stop (TRIGGER_MARKET):`) console.log(` Trigger: $${options.hardStopPrice.toFixed(4)}`) console.log(` Purpose: Guaranteed exit if soft stop doesn't fill`) const hardStopSig = await retryWithBackoff(async () => await (driftClient as any).placePerpOrder(hardStopParams) ) console.log(` โœ… Hard stop placed: ${hardStopSig}`) signatures.push(hardStopSig) console.log(`๐ŸŽฏ Dual stop system active: Soft @ $${options.softStopPrice.toFixed(2)} | Hard @ $${options.hardStopPrice.toFixed(2)}`) } else { // ============== SINGLE STOP SYSTEM ============== const useStopLimit = options.useStopLimit ?? false const stopLimitBuffer = options.stopLimitBuffer ?? 0.5 if (useStopLimit) { // TRIGGER_LIMIT: For liquid markets const limitPriceMultiplier = options.direction === 'long' ? (1 - stopLimitBuffer / 100) : (1 + stopLimitBuffer / 100) const orderParams: any = { orderType: OrderType.TRIGGER_LIMIT, marketIndex: marketConfig.driftMarketIndex, direction: orderDirection, baseAssetAmount: new BN(slBaseAmount), triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)), price: new BN(Math.floor(options.stopLossPrice * limitPriceMultiplier * 1e6)), triggerCondition: options.direction === 'long' ? OrderTriggerCondition.BELOW : OrderTriggerCondition.ABOVE, reduceOnly: true, } console.log(`๐Ÿ›ก๏ธ Placing SL as TRIGGER_LIMIT (${stopLimitBuffer}% buffer)...`) console.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`) console.log(` Limit: $${(options.stopLossPrice * limitPriceMultiplier).toFixed(4)}`) console.log(` โš ๏ธ May not fill during fast moves - use for liquid markets only!`) const sig = await retryWithBackoff(async () => await (driftClient as any).placePerpOrder(orderParams) ) console.log('โœ… SL trigger-limit order placed:', sig) signatures.push(sig) } else { // TRIGGER_MARKET: Default, guaranteed execution const orderParams: any = { orderType: OrderType.TRIGGER_MARKET, marketIndex: marketConfig.driftMarketIndex, direction: orderDirection, baseAssetAmount: new BN(slBaseAmount), triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)), triggerCondition: options.direction === 'long' ? OrderTriggerCondition.BELOW : OrderTriggerCondition.ABOVE, reduceOnly: true, } console.log(`๐Ÿ›ก๏ธ Placing SL as TRIGGER_MARKET (guaranteed execution - RECOMMENDED)...`) console.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`) console.log(` โœ… Will execute at market price when triggered (may slip but WILL fill)`) const sig = await retryWithBackoff(async () => await (driftClient as any).placePerpOrder(orderParams) ) console.log('โœ… SL trigger-market order placed:', sig) signatures.push(sig) } } } else { console.log('โš ๏ธ SL size below market min, skipping on-chain SL') } return { success: true, signatures } } catch (error) { console.error('โŒ Failed to place exit orders:', error) return { success: false, error: error instanceof Error ? error.message : 'Unknown error' } } } /** * Close a position (partially or fully) with a market order */ export async function closePosition( params: ClosePositionParams ): Promise { try { console.log('๐Ÿ“Š Closing position:', params) const driftService = getDriftService() const marketConfig = getMarketConfig(params.symbol) const driftClient = driftService.getClient() // Get current position const position = await driftService.getPosition(marketConfig.driftMarketIndex) if (!position || position.side === 'none') { throw new Error(`No active position for ${params.symbol}`) } // Calculate size to close let sizeToClose = position.size * (params.percentToClose / 100) // CRITICAL FIX: If calculated size is below minimum, close 100% instead // This prevents "runner" positions from being too small to close if (sizeToClose < marketConfig.minOrderSize) { console.log(`โš ๏ธ Calculated close size ${sizeToClose.toFixed(4)} is below minimum ${marketConfig.minOrderSize}`) console.log(` Forcing 100% close to avoid Drift rejection`) sizeToClose = position.size // Close entire position } console.log(`๐Ÿ“ Close order details:`) console.log(` Current position: ${position.size.toFixed(4)} ${position.side}`) console.log(` Closing: ${params.percentToClose}% (${sizeToClose.toFixed(4)})`) console.log(` Entry price: $${position.entryPrice.toFixed(4)}`) console.log(` Unrealized P&L: $${position.unrealizedPnL.toFixed(2)}`) // Get current oracle price const oraclePrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex) console.log(` Current price: $${oraclePrice.toFixed(4)}`) // Check DRY_RUN mode const isDryRun = process.env.DRY_RUN === 'true' if (isDryRun) { console.log('๐Ÿงช DRY RUN MODE: Simulating close order (not executing on blockchain)') // Calculate realized P&L with leverage (default 10x in dry run) const profitPercent = ((oraclePrice - position.entryPrice) / position.entryPrice) * 100 * (position.side === 'long' ? 1 : -1) const closedNotional = sizeToClose * oraclePrice const realizedPnL = (closedNotional * profitPercent) / 100 const accountPnLPercent = profitPercent * 10 // display using default leverage const mockTxSig = `DRY_RUN_CLOSE_${Date.now()}_${Math.random().toString(36).substring(7)}` console.log(`๐Ÿ’ฐ Simulated close:`) console.log(` Close price: $${oraclePrice.toFixed(4)}`) console.log(` Profit %: ${profitPercent.toFixed(3)}% โ†’ Account P&L (10x): ${accountPnLPercent.toFixed(2)}%`) console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`) return { success: true, transactionSignature: mockTxSig, closePrice: oraclePrice, closedSize: sizeToClose, realizedPnL, } } // Prepare close order (opposite direction) - use simple structure like v3 const orderParams = { orderType: OrderType.MARKET, marketIndex: marketConfig.driftMarketIndex, direction: position.side === 'long' ? PositionDirection.SHORT : PositionDirection.LONG, baseAssetAmount: new BN(Math.floor(sizeToClose * 1e9)), // 9 decimals reduceOnly: true, // Important: only close existing position } // Place market close order using simple placePerpOrder (like v3) // CRITICAL: Wrap in retry logic for rate limit protection console.log('๐Ÿš€ Placing REAL market close order with retry protection...') const txSig = await retryWithBackoff(async () => { return await driftClient.placePerpOrder(orderParams) }, 3, 8000) // 8s base delay, 3 max retries console.log(`โœ… Close order placed! Transaction: ${txSig}`) // CRITICAL: Confirm transaction on-chain to prevent phantom closes // BUT: Use timeout to prevent API hangs during network congestion console.log('โณ Confirming transaction on-chain (30s timeout)...') const connection = driftService.getTradeConnection() // Use Alchemy for trade operations try { const confirmationPromise = connection.confirmTransaction(txSig, 'confirmed') const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Transaction confirmation timeout')), 30000) ) const confirmation = await Promise.race([confirmationPromise, timeoutPromise]) as any if (confirmation.value?.err) { console.error('โŒ Transaction failed on-chain:', confirmation.value.err) throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`) } console.log('โœ… Transaction confirmed on-chain') } catch (timeoutError: any) { if (timeoutError.message === 'Transaction confirmation timeout') { console.warn('โš ๏ธ Transaction confirmation timed out after 30s') console.warn(' Order may still execute - check Drift UI') console.warn(` Transaction signature: ${txSig}`) // Continue anyway - order was submitted and will likely execute } else { throw timeoutError } } // Calculate realized P&L with leverage // CRITICAL: P&L must account for leverage and be calculated on USD notional, not base asset size const profitPercent = ((oraclePrice - position.entryPrice) / position.entryPrice) * 100 * (position.side === 'long' ? 1 : -1) // Get leverage from user account (defaults to 10x if not found) let leverage = 10 try { const userAccount = driftClient.getUserAccount() if (userAccount && userAccount.maxMarginRatio) { // maxMarginRatio is in 1e4 scale, leverage = 1 / (margin / 10000) leverage = 10000 / Number(userAccount.maxMarginRatio) } } catch (err) { console.log('โš ๏ธ Could not determine leverage from account, using 10x default') } // Calculate closed notional value (USD) const closedNotional = sizeToClose * oraclePrice const realizedPnL = (closedNotional * profitPercent) / 100 const accountPnLPercent = profitPercent * leverage console.log(`๐Ÿ’ฐ Close details:`) console.log(` Close price: $${oraclePrice.toFixed(4)}`) console.log(` Profit %: ${profitPercent.toFixed(3)}% | Account P&L (${leverage}x): ${accountPnLPercent.toFixed(2)}%`) console.log(` Closed notional: $${closedNotional.toFixed(2)}`) console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`) // If closing 100%, verify position actually closed and cancel remaining orders if (params.percentToClose === 100) { console.log('๐Ÿ—‘๏ธ Position fully closed, cancelling remaining orders...') const cancelResult = await cancelAllOrders(params.symbol) if (cancelResult.success && cancelResult.cancelledCount! > 0) { console.log(`โœ… Cancelled ${cancelResult.cancelledCount} orders`) } // CRITICAL: Verify position actually closed on Drift (Nov 16, 2025) // Transaction confirmed โ‰  Drift state updated immediately // Wait 5 seconds for Drift internal state to propagate console.log('โณ Waiting 5s for Drift state to propagate...') await new Promise(resolve => setTimeout(resolve, 5000)) try { const verifyPosition = await driftService.getPosition(marketConfig.driftMarketIndex) if (verifyPosition && Math.abs(verifyPosition.size) >= 0.01) { console.error(`๐Ÿ”ด CRITICAL: Close transaction confirmed BUT position still exists on Drift!`) console.error(` Transaction: ${txSig}`) console.error(` Drift size: ${verifyPosition.size}`) console.error(` This indicates Drift state propagation delay or partial fill`) console.error(` Position Manager will continue monitoring until Drift confirms closure`) // Return success but flag that monitoring should continue return { success: true, transactionSignature: txSig, closePrice: oraclePrice, closedSize: sizeToClose, realizedPnL, needsVerification: true, // Flag for Position Manager } } else { console.log('โœ… Position verified closed on Drift') } } catch (verifyError) { console.warn('โš ๏ธ Could not verify position closure:', verifyError) // Continue anyway - transaction was confirmed } } return { success: true, transactionSignature: txSig, closePrice: oraclePrice, closedSize: sizeToClose, realizedPnL, } } catch (error) { console.error('โŒ Failed to close position:', error) return { success: false, error: error instanceof Error ? error.message : 'Unknown error', } } } /** * Cancel all open orders for a specific market */ /** * Retry a function with exponential backoff for rate limit errors * * Helius RPC limits (free tier): * - 100 requests/second burst * - 10 requests/second sustained * * Strategy: Longer delays to avoid overwhelming RPC during rate limit situations */ async function retryWithBackoff( fn: () => Promise, maxRetries: number = 3, baseDelay: number = 8000 // Increased from 5s to 8s: 8s โ†’ 16s โ†’ 32s progression for better RPC recovery ): Promise { const startTime = Date.now() for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const result = await fn() // Log successful execution time for rate limit monitoring if (attempt > 0) { const totalTime = Date.now() - startTime console.log(`โœ… Retry successful after ${totalTime}ms (${attempt} retries)`) // Log to database for analytics try { const { logSystemEvent } = await import('../database/trades') await logSystemEvent('rate_limit_recovered', 'Drift RPC rate limit recovered after retries', { retriesNeeded: attempt, totalTimeMs: totalTime, recoveredAt: new Date().toISOString(), }) } catch (dbError) { console.error('Failed to log rate limit recovery:', dbError) } } return result } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) const isRateLimit = errorMessage.includes('429') || errorMessage.includes('rate limit') if (!isRateLimit || attempt === maxRetries) { // Log final failure with full context if (isRateLimit && attempt === maxRetries) { const totalTime = Date.now() - startTime console.error(`โŒ RATE LIMIT EXHAUSTED: Failed after ${maxRetries} retries and ${totalTime}ms`) console.error(` Error: ${errorMessage}`) // Log to database for analytics try { const { logSystemEvent } = await import('../database/trades') await logSystemEvent('rate_limit_exhausted', 'Drift RPC rate limit exceeded max retries', { maxRetries, totalTimeMs: totalTime, errorMessage: errorMessage.substring(0, 500), failedAt: new Date().toISOString(), }) } catch (dbError) { console.error('Failed to log rate limit exhaustion:', dbError) } } throw error } const delay = baseDelay * Math.pow(2, attempt) console.log(`โณ Rate limited (429), retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${maxRetries})`) console.log(` Error context: ${errorMessage.substring(0, 100)}`) // Log rate limit hit to database try { const { logSystemEvent } = await import('../database/trades') await logSystemEvent('rate_limit_hit', 'Drift RPC rate limit encountered', { attempt: attempt + 1, maxRetries, delayMs: delay, errorSnippet: errorMessage.substring(0, 200), hitAt: new Date().toISOString(), }) } catch (dbError) { console.error('Failed to log rate limit hit:', dbError) } await new Promise(resolve => setTimeout(resolve, delay)) } } throw new Error('Max retries reached') } export async function cancelAllOrders( symbol: string ): Promise<{ success: boolean; cancelledCount?: number; error?: string }> { try { console.log(`๐Ÿ—‘๏ธ Cancelling all orders for ${symbol}...`) // Ensure Drift service is initialized let driftService = getDriftService() if (!driftService) { console.log('โš ๏ธ Drift service not initialized, initializing now...') driftService = await initializeDriftService() } const driftClient = driftService.getClient() const marketConfig = getMarketConfig(symbol) const isDryRun = process.env.DRY_RUN === 'true' if (isDryRun) { console.log('๐Ÿงช DRY RUN: Simulating order cancellation') return { success: true, cancelledCount: 0 } } // Get user account to check for orders const userAccount = driftClient.getUserAccount() if (!userAccount) { throw new Error('User account not found') } // Filter orders for this market (check for TRULY active orders) // CRITICAL: Empty slots have orderId=0 OR baseAssetAmount=0 // Only count orders that actually exist and are open const ordersToCancel = userAccount.orders.filter( (order: any) => { // Skip if not our market if (order.marketIndex !== marketConfig.driftMarketIndex) return false // Skip if orderId is 0 (empty slot) if (!order.orderId || order.orderId === 0) return false // Skip if baseAssetAmount is 0 (no actual order size) if (!order.baseAssetAmount || order.baseAssetAmount.eq(new BN(0))) return false // This is a real active order return true } ) if (ordersToCancel.length === 0) { console.log('โœ… No open orders to cancel') return { success: true, cancelledCount: 0 } } console.log(`๐Ÿ“‹ Found ${ordersToCancel.length} open orders to cancel (including trigger orders)`) console.log(` (checked ${userAccount.orders.length} total order slots)`) // Cancel all orders with retry logic for rate limits const txSig = await retryWithBackoff(async () => { return await driftClient.cancelOrders( undefined, // Cancel by market type marketConfig.driftMarketIndex, undefined // No specific direction filter ) }) console.log(`โœ… Orders cancelled! Transaction: ${txSig}`) return { success: true, cancelledCount: ordersToCancel.length, } } catch (error) { console.error('โŒ Failed to cancel orders:', error) return { success: false, error: error instanceof Error ? error.message : 'Unknown error', } } } /** * Close entire position for a market */ export async function closeEntirePosition( symbol: string, slippageTolerance: number = 1.0 ): Promise { return closePosition({ symbol, percentToClose: 100, slippageTolerance, }) } /** * Emergency close all positions */ export async function emergencyCloseAll(): Promise<{ success: boolean results: Array<{ symbol: string result: ClosePositionResult }> }> { console.log('๐Ÿšจ EMERGENCY: Closing all positions') try { const driftService = getDriftService() const positions = await driftService.getAllPositions() if (positions.length === 0) { console.log('โœ… No positions to close') return { success: true, results: [] } } const results = [] for (const position of positions) { console.log(`๐Ÿ”ด Emergency closing ${position.symbol}...`) const result = await closeEntirePosition(position.symbol, 2.0) // Allow 2% slippage results.push({ symbol: position.symbol, result, }) } console.log('โœ… Emergency close complete') return { success: true, results, } } catch (error) { console.error('โŒ Emergency close failed:', error) return { success: false, results: [], } } }