diff --git a/lib/safety/sl-verification.ts b/lib/safety/sl-verification.ts index 96afc28..d9b12cc 100644 --- a/lib/safety/sl-verification.ts +++ b/lib/safety/sl-verification.ts @@ -90,6 +90,106 @@ export async function querySLOrdersFromDrift( } } +/** + * Attempt to place SL orders for a position that's missing them + * Loads trade from database, calls placeExitOrders with SL-only params + * Updates database with SL signatures if successful + * + * @returns true if SL orders placed successfully, false otherwise + */ +async function attemptSLPlacement( + tradeId: string, + symbol: string, + marketIndex: number +): Promise { + try { + console.log(`🔧 Attempting to place SL orders for ${symbol} (Trade: ${tradeId})`) + + // Load trade from database + const { getPrismaClient } = await import('../database/trades') + const prisma = getPrismaClient() + const trade = await prisma.trade.findUnique({ + where: { id: tradeId }, + select: { + positionSizeUSD: true, + entryPrice: true, + stopLossPrice: true, + direction: true, + takeProfit1Price: true, + takeProfit2Price: true, + } + }) + + if (!trade) { + console.error(`❌ Trade ${tradeId} not found in database`) + return false + } + + if (!trade.stopLossPrice) { + console.error(`❌ Trade ${tradeId} has no stopLossPrice set`) + return false + } + + // Import placeExitOrders dynamically + const { placeExitOrders } = await import('../drift/orders') + + // Use stored TP prices, fallback to entry if missing + const tp1Price = trade.takeProfit1Price || trade.entryPrice + const tp2Price = trade.takeProfit2Price || trade.entryPrice + + // Place SL orders only (tp1SizePercent=0, tp2SizePercent=0) + console.log(` Placing SL-only orders (preserving existing TP orders)`) + const result = await placeExitOrders({ + symbol, + positionSizeUSD: trade.positionSizeUSD, + entryPrice: trade.entryPrice, + tp1Price: tp1Price, + tp2Price: tp2Price, + stopLossPrice: trade.stopLossPrice, + tp1SizePercent: 0, // Don't place TP1 order + tp2SizePercent: 0, // Don't place TP2 order + direction: trade.direction as 'long' | 'short', + }) + + if (!result.success) { + console.error(`❌ placeExitOrders failed:`, result.error) + return false + } + + if (!result.signatures || result.signatures.length === 0) { + console.error(`❌ placeExitOrders returned success but no signatures`) + return false + } + + // Update database with SL signatures + const updateData: any = {} + + // Check if dual stops are being used (2 signatures) or single SL (1 signature) + if (result.signatures.length === 2) { + // Dual stops: soft SL + hard SL + updateData.softStopOrderTx = result.signatures[0] + updateData.hardStopOrderTx = result.signatures[1] + console.log(` Placed 2 SL orders: soft (${result.signatures[0]}) + hard (${result.signatures[1]})`) + } else { + // Single SL + updateData.slOrderTx = result.signatures[0] + console.log(` Placed 1 SL order: ${result.signatures[0]}`) + } + + await prisma.trade.update({ + where: { id: tradeId }, + data: updateData + }) + + console.log(`✅ SL orders placed and database updated for ${symbol}`) + return true + + } catch (error) { + console.error(`❌ Error in attemptSLPlacement:`, error) + return false + } +} + /** * Halt trading and close position immediately * Called when SL verification fails after all retries @@ -166,8 +266,11 @@ MANUAL INTERVENTION REQUIRED IMMEDIATELY`) } /** - * Verify SL orders exist with exponential backoff (30s, 60s, 90s) - * If all 3 attempts fail: Halt trading + close position + * Verify SL orders exist with hybrid approach: passive check → active recovery + * - 30s: Passive check only (give initial placement time to propagate) + * - 60s: Check + try to place SL if missing (active recovery attempt 1) + * - 90s: Check + try to place SL if missing (active recovery attempt 2) + * - If both 60s and 90s recovery attempts fail: Halt trading + close position * * Usage: Call after position opened successfully * Example: await verifySLWithRetries(tradeId, symbol, marketIndex) @@ -178,49 +281,79 @@ export async function verifySLWithRetries( marketIndex: number ): Promise { const delays = [30000, 60000, 90000] // 30s, 60s, 90s - const maxAttempts = 3 console.log(`🛡️ Starting SL verification for ${symbol} (Trade: ${tradeId})`) - console.log(` Verification schedule: 30s, 60s, 90s (3 attempts)`) + console.log(` Strategy: 30s passive → 60s/90s active recovery if needed`) - for (let attempt = 1; attempt <= maxAttempts; attempt++) { - const delay = delays[attempt - 1] - - console.log(`⏱️ Verification attempt ${attempt}/${maxAttempts} - waiting ${delay/1000}s...`) - - // Wait for scheduled delay - await new Promise(resolve => setTimeout(resolve, delay)) - - // Query Drift on-chain state - const slStatus = await querySLOrdersFromDrift(symbol, marketIndex) - - if (slStatus.exists) { - console.log(`✅ SL VERIFIED on attempt ${attempt}/${maxAttempts}`) - console.log(` Found ${slStatus.orderCount} SL order(s): ${slStatus.orderTypes.join(', ')}`) - console.log(` Verification timing: ${delay/1000}s after position open`) - - // Success - verification details logged above - return // Success - exit retry loop - } - - console.warn(`⚠️ SL NOT FOUND on attempt ${attempt}/${maxAttempts}`) - console.warn(` Reduce-only orders: ${slStatus.orderCount}`) - - if (attempt < maxAttempts) { - console.log(` Retrying in ${delays[attempt]/1000}s...`) - } else { - // All 3 attempts failed - CRITICAL FAILURE - console.error(`❌ SL VERIFICATION FAILED after ${maxAttempts} attempts`) - console.error(` Position is UNPROTECTED - initiating emergency procedures`) - - // Halt trading + close position - await haltTradingAndClosePosition( - tradeId, - symbol, - `SL verification failed after ${maxAttempts} attempts (30s, 60s, 90s). Position left unprotected - Bug #76 detected.` - ) - } + // ATTEMPT 1 (30s): Passive check only + console.log(`⏱️ Verification attempt 1/3 (PASSIVE CHECK) - waiting 30s...`) + await new Promise(resolve => setTimeout(resolve, delays[0])) + + let slStatus = await querySLOrdersFromDrift(symbol, marketIndex) + + if (slStatus.exists) { + console.log(`✅ SL VERIFIED on attempt 1/3 (passive check)`) + console.log(` Found ${slStatus.orderCount} SL order(s): ${slStatus.orderTypes.join(', ')}`) + return // Success - SL orders exist } + + console.error(`❌ SL NOT FOUND on attempt 1/3 - will attempt active recovery`) + + // ATTEMPT 2 (60s): Active recovery - try to place SL + console.log(`⏱️ Verification attempt 2/3 (ACTIVE RECOVERY) - waiting 60s...`) + await new Promise(resolve => setTimeout(resolve, delays[1])) + + // Check again first + slStatus = await querySLOrdersFromDrift(symbol, marketIndex) + + if (slStatus.exists) { + console.log(`✅ SL VERIFIED on attempt 2/3 (appeared naturally)`) + console.log(` Found ${slStatus.orderCount} SL order(s): ${slStatus.orderTypes.join(', ')}`) + return // Success - SL orders now exist + } + + console.error(`❌ SL still missing on attempt 2/3 - attempting to place SL orders...`) + const recovery2Success = await attemptSLPlacement(tradeId, symbol, marketIndex) + + if (recovery2Success) { + console.log(`✅ SL PLACED successfully on attempt 2/3 (active recovery)`) + return // Success - SL orders placed + } + + console.error(`❌ SL placement FAILED on attempt 2/3`) + + // ATTEMPT 3 (90s): Final active recovery - try to place SL again + console.log(`⏱️ Verification attempt 3/3 (FINAL RECOVERY) - waiting 90s...`) + await new Promise(resolve => setTimeout(resolve, delays[2])) + + // Check one more time + slStatus = await querySLOrdersFromDrift(symbol, marketIndex) + + if (slStatus.exists) { + console.log(`✅ SL VERIFIED on attempt 3/3 (appeared naturally)`) + console.log(` Found ${slStatus.orderCount} SL order(s): ${slStatus.orderTypes.join(', ')}`) + return // Success - SL orders now exist + } + + console.error(`❌ SL still missing on attempt 3/3 - final placement attempt...`) + const recovery3Success = await attemptSLPlacement(tradeId, symbol, marketIndex) + + if (recovery3Success) { + console.log(`✅ SL PLACED successfully on attempt 3/3 (final recovery)`) + return // Success - SL orders placed + } + + // BOTH RECOVERY ATTEMPTS FAILED - EMERGENCY SHUTDOWN + console.error(`🚨 CRITICAL: SL verification + both recovery attempts FAILED`) + console.error(` Symbol: ${symbol}`) + console.error(` Trade ID: ${tradeId}`) + console.error(` Action: Halting trading + closing position`) + + await haltTradingAndClosePosition( + tradeId, + symbol, + `SL verification failed - both 60s and 90s recovery attempts unsuccessful` + ) } /**