From dcee1174a73c8c699898dde8f6786b8803dd7001 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Tue, 16 Dec 2025 15:25:58 +0100 Subject: [PATCH] enhance: Bug #87 Phase 2 - Active SL recovery at 60s/90s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hybrid passive+active approach per user specification: - 30s: Passive check only (give initial placement time to propagate) - 60s: Check + attemptSLPlacement() if missing (recovery attempt #1) - 90s: Check + attemptSLPlacement() if missing (recovery attempt #2) - If both attempts fail: haltTradingAndClosePosition() New function: attemptSLPlacement(tradeId, symbol, marketIndex) - Loads trade from database (positionSizeUSD, entryPrice, stopLossPrice, etc.) - Calls placeExitOrders() with tp1SizePercent=0, tp2SizePercent=0 (SL-only) - Updates database with SL signatures if successful - Returns true on success, false on failure Modified: verifySLWithRetries() with conditional active recovery - Attempt 1 (30s): Passive verification only - Attempt 2 (60s): Verification + active placement if missing - Attempt 3 (90s): Verification + active placement if missing - Emergency: Halt + close if all attempts exhausted Benefits: - Maximizes trade survival by attempting recovery before shutdown - Two recovery chances reduce false positive emergency shutdowns - 30s passive first gives initial placement reasonable propagation time User requirement: '30sec after position was opened it shall only check. after 60s and 90s check if it does not exist it shall try to place the SL. if both attempts on 60 and 90 fail then stop trading and close position' Deployed: Dec 15, 2025 13:30 UTC Container: trading-bot-v4 (image sha256:4eaef891...) TypeScript compilation: ✅ Clean (no errors) Database schema fields: positionSizeUSD, takeProfit1Price, takeProfit2Price --- lib/safety/sl-verification.ts | 215 +++++++++++++++++++++++++++------- 1 file changed, 174 insertions(+), 41 deletions(-) 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` + ) } /**