/** * Drift Order Execution * * Handles opening and closing positions with market orders */ import { getDriftService } 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 } export interface ClosePositionParams { symbol: string percentToClose: number // 0-100 slippageTolerance: number } export interface ClosePositionResult { success: boolean transactionSignature?: string closePrice?: number closedSize?: number realizedPnL?: number error?: string } export interface PlaceExitOrdersResult { success: boolean signatures?: string[] error?: string } export interface PlaceExitOrdersOptions { symbol: string positionSizeUSD: number 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(`โœ… Order placed! Transaction: ${txSig}`) // 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 (optional - may not be immediate in DRY_RUN) const position = await driftService.getPosition(marketConfig.driftMarketIndex) if (position && position.side !== 'none') { const fillPrice = position.entryPrice const slippage = Math.abs((fillPrice - oraclePrice) / oraclePrice) * 100 console.log(`๐Ÿ’ฐ Fill details:`) console.log(` Fill price: $${fillPrice.toFixed(4)}`) console.log(` Slippage: ${slippage.toFixed(3)}%`) return { success: true, transactionSignature: txSig, fillPrice, fillSize: baseAssetSize, slippage, } } 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 const usdToBase = (usd: number, price: number) => { const base = usd / price return Math.floor(base * 1e9) // 9 decimals expected by SDK } // Calculate sizes in USD for each TP const tp1USD = (options.positionSizeUSD * options.tp1SizePercent) / 100 const tp2USD = (options.positionSizeUSD * options.tp2SizePercent) / 100 // 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, options.tp1Price) 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 (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, options.tp2Price) 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 (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, options.stopLossPrice) 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 (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 (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 (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 (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 const sizeToClose = position.size * (params.percentToClose / 100) 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 const pnlPerUnit = oraclePrice - position.entryPrice const realizedPnL = pnlPerUnit * sizeToClose * (position.side === 'long' ? 1 : -1) 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(` 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) console.log('๐Ÿš€ Placing REAL market close order...') const txSig = await driftClient.placePerpOrder(orderParams) console.log(`โœ… Close order placed! Transaction: ${txSig}`) // Wait for confirmation (transaction is likely already confirmed by placeAndTakePerpOrder) console.log('โณ Waiting for transaction confirmation...') console.log('โœ… Transaction confirmed') // Calculate realized P&L const pnlPerUnit = oraclePrice - position.entryPrice const realizedPnL = pnlPerUnit * sizeToClose * (position.side === 'long' ? 1 : -1) console.log(`๐Ÿ’ฐ Close details:`) console.log(` Close price: $${oraclePrice.toFixed(4)}`) console.log(` Realized P&L: $${realizedPnL.toFixed(2)}`) // If closing 100%, cancel all remaining orders for this market 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`) } } 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 */ export async function cancelAllOrders( symbol: string ): Promise<{ success: boolean; cancelledCount?: number; error?: string }> { try { console.log(`๐Ÿ—‘๏ธ Cancelling all orders for ${symbol}...`) const driftService = getDriftService() 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 const ordersToCancel = userAccount.orders.filter( (order: any) => order.marketIndex === marketConfig.driftMarketIndex && order.status === 0 // 0 = Open status ) 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`) // Cancel all orders for this market const txSig = 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: [], } } }