enhance: Bug #87 Phase 2 - Active SL recovery at 60s/90s

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
This commit is contained in:
mindesbunister
2025-12-16 15:25:58 +01:00
parent c77d236842
commit dcee1174a7

View File

@@ -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<boolean> {
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 * Halt trading and close position immediately
* Called when SL verification fails after all retries * 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) * Verify SL orders exist with hybrid approach: passive check → active recovery
* If all 3 attempts fail: Halt trading + close position * - 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 * Usage: Call after position opened successfully
* Example: await verifySLWithRetries(tradeId, symbol, marketIndex) * Example: await verifySLWithRetries(tradeId, symbol, marketIndex)
@@ -178,49 +281,79 @@ export async function verifySLWithRetries(
marketIndex: number marketIndex: number
): Promise<void> { ): Promise<void> {
const delays = [30000, 60000, 90000] // 30s, 60s, 90s const delays = [30000, 60000, 90000] // 30s, 60s, 90s
const maxAttempts = 3
console.log(`🛡️ Starting SL verification for ${symbol} (Trade: ${tradeId})`) 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++) { // ATTEMPT 1 (30s): Passive check only
const delay = delays[attempt - 1] console.log(`⏱️ Verification attempt 1/3 (PASSIVE CHECK) - waiting 30s...`)
await new Promise(resolve => setTimeout(resolve, delays[0]))
console.log(`⏱️ Verification attempt ${attempt}/${maxAttempts} - waiting ${delay/1000}s...`)
let slStatus = await querySLOrdersFromDrift(symbol, marketIndex)
// Wait for scheduled delay
await new Promise(resolve => setTimeout(resolve, delay)) if (slStatus.exists) {
console.log(`✅ SL VERIFIED on attempt 1/3 (passive check)`)
// Query Drift on-chain state console.log(` Found ${slStatus.orderCount} SL order(s): ${slStatus.orderTypes.join(', ')}`)
const slStatus = await querySLOrdersFromDrift(symbol, marketIndex) return // Success - SL orders exist
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.`
)
}
} }
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`
)
} }
/** /**