diff --git a/lib/drift/orders.ts b/lib/drift/orders.ts index 8109cb0..35930e2 100644 --- a/lib/drift/orders.ts +++ b/lib/drift/orders.ts @@ -52,6 +52,19 @@ export interface PlaceExitOrdersResult { 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%) +} + /** * Open a position with a market order */ @@ -171,21 +184,16 @@ export async function openPosition( /** - * Place on-chain exit orders (reduce-only LIMIT orders) so TP/SL show up in Drift UI. - * This places reduce-only LIMIT orders for TP1, TP2 and a stop-loss LIMIT order. - * NOTE: For a safer, more aggressive stop you'd want a trigger-market order; here - * we use reduce-only LIMIT orders to ensure they are visible in the UI and low-risk. + * 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: { - symbol: string - positionSizeUSD: number - tp1Price: number - tp2Price: number - stopLossPrice: number - tp1SizePercent: number // percent of position to close at TP1 (0-100) - tp2SizePercent: number // percent of position to close at TP2 (0-100) - direction: 'long' | 'short' -}): Promise { +export async function placeExitOrders(options: PlaceExitOrdersOptions): Promise { try { console.log('🛡️ Placing exit orders on-chain:', options.symbol) @@ -265,29 +273,65 @@ export async function placeExitOrders(options: { } } - // Place Stop-Loss as TRIGGER_LIMIT order (proper stop loss that shows on Drift UI) - const slUSD = options.positionSizeUSD // place full-size SL + // Place Stop-Loss order + // Default: TRIGGER_MARKET (guaranteed execution, RECOMMENDED for most traders) + // Optional: TRIGGER_LIMIT with buffer (only for very liquid markets to avoid extreme wicks) + const slUSD = options.positionSizeUSD const slBaseAmount = usdToBase(slUSD, options.stopLossPrice) + if (slBaseAmount >= Math.floor(marketConfig.minOrderSize * 1e9)) { - const orderParams: any = { - orderType: OrderType.TRIGGER_LIMIT, // Use TRIGGER_LIMIT (not LIMIT with trigger) - marketIndex: marketConfig.driftMarketIndex, - direction: orderDirection, - baseAssetAmount: new BN(slBaseAmount), - triggerPrice: new BN(Math.floor(options.stopLossPrice * 1e6)), // trigger price - price: new BN(Math.floor(options.stopLossPrice * 0.995 * 1e6)), // limit price slightly lower (0.5% buffer) - triggerCondition: options.direction === 'long' - ? OrderTriggerCondition.BELOW // Long: trigger when price drops below - : OrderTriggerCondition.ABOVE, // Short: trigger when price rises above - reduceOnly: true, + const useStopLimit = options.useStopLimit ?? false + const stopLimitBuffer = options.stopLimitBuffer ?? 0.5 // default 0.5% buffer + + if (useStopLimit) { + // TRIGGER_LIMIT: Protects against extreme wicks but may not fill during fast moves + const limitPriceMultiplier = options.direction === 'long' + ? (1 - stopLimitBuffer / 100) // Long: limit below trigger + : (1 + stopLimitBuffer / 100) // Short: limit above trigger + + 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: Guaranteed execution (RECOMMENDED) + 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) } - - console.log('🚧 Placing SL trigger-limit order (reduce-only)...') - console.log(` Trigger: ${options.direction === 'long' ? 'BELOW' : 'ABOVE'} $${options.stopLossPrice.toFixed(4)}`) - console.log(` Limit price: $${(options.stopLossPrice * 0.995).toFixed(4)}`) - const sig = await (driftClient as any).placePerpOrder(orderParams) - console.log('✅ SL trigger-limit order placed:', sig) - signatures.push(sig) } else { console.log('⚠️ SL size below market min, skipping on-chain SL') }