/** * Position Manager Health Check * * CRITICAL: Verifies Position Manager is actually monitoring positions * * Bug History: * - $1,000+ losses because Position Manager logged "added" but never monitored * - Silent SL placement failures left positions unprotected * - Orphan detection cancelled orders on active positions * * This health check runs every 30 seconds to detect these critical failures. * * Created: Dec 8, 2025 */ import { getInitializedPositionManager } from '../trading/position-manager' import { getOpenTrades } from '../database/trades' import { getDriftService } from '../drift/client' export interface HealthCheckResult { isHealthy: boolean issues: string[] warnings: string[] info: { dbOpenTrades: number pmActiveTrades: number pmMonitoring: boolean driftPositions: number unprotectedPositions: number } } /** * Check Position Manager health * * CRITICAL CHECKS: * 1. If DB has open trades, Position Manager MUST be monitoring * 2. If Position Manager has trades, monitoring MUST be active * 3. All open positions MUST have TP/SL orders on-chain * 4. Position Manager trade count MUST match Drift position count */ export async function checkPositionManagerHealth(): Promise { const issues: string[] = [] const warnings: string[] = [] try { // Get database open trades const dbTrades = await getOpenTrades() const dbOpenCount = dbTrades.length // Get Position Manager state const pm = await getInitializedPositionManager() const pmState = (pm as any) const pmActiveTrades = pmState.activeTrades?.size || 0 const pmMonitoring = pmState.isMonitoring || false // Get Drift positions const driftService = getDriftService() const positions = await driftService.getAllPositions() const driftPositions = positions.filter(p => Math.abs(p.size) > 0).length // CRITICAL CHECK #1: DB has open trades but PM not monitoring if (dbOpenCount > 0 && !pmMonitoring) { issues.push(`❌ CRITICAL: ${dbOpenCount} open trades in DB but Position Manager NOT monitoring!`) issues.push(` This means NO TP/SL protection, NO monitoring, UNCONTROLLED RISK`) issues.push(` ACTION REQUIRED: Restart container to restore monitoring`) } // CRITICAL CHECK #2: PM has trades but not monitoring if (pmActiveTrades > 0 && !pmMonitoring) { issues.push(`❌ CRITICAL: Position Manager has ${pmActiveTrades} active trades but monitoring is OFF!`) issues.push(` This is the $1,000 loss bug - trades "added" but never monitored`) issues.push(` ACTION REQUIRED: Fix startMonitoring() function`) } // CRITICAL CHECK #3: DB vs PM mismatch if (dbOpenCount !== pmActiveTrades) { warnings.push(`⚠️ WARNING: DB has ${dbOpenCount} open trades, PM has ${pmActiveTrades} active trades`) warnings.push(` Possible orphaned position or monitoring not started`) } // CRITICAL CHECK #4: PM vs Drift mismatch if (pmActiveTrades !== driftPositions) { warnings.push(`⚠️ WARNING: Position Manager has ${pmActiveTrades} trades, Drift has ${driftPositions} positions`) warnings.push(` Possible untracked position or external closure`) } // Check for unprotected positions (no SL/TP orders) let unprotectedPositions = 0 for (const trade of dbTrades) { if (!trade.slOrderTx && !trade.softStopOrderTx && !trade.hardStopOrderTx) { unprotectedPositions++ issues.push(`❌ CRITICAL: Position ${trade.symbol} (${trade.id}) has NO STOP LOSS ORDERS!`) issues.push(` Entry: $${trade.entryPrice}, Size: $${trade.positionSizeUSD}`) issues.push(` This is the silent SL placement failure bug`) } if (!trade.tp1OrderTx) { warnings.push(`⚠️ Position ${trade.symbol} missing TP1 order`) } if (!trade.tp2OrderTx) { warnings.push(`⚠️ Position ${trade.symbol} missing TP2 order`) } } const isHealthy = issues.length === 0 return { isHealthy, issues, warnings, info: { dbOpenTrades: dbOpenCount, pmActiveTrades, pmMonitoring, driftPositions, unprotectedPositions } } } catch (error) { issues.push(`❌ Health check failed: ${error instanceof Error ? error.message : String(error)}`) return { isHealthy: false, issues, warnings, info: { dbOpenTrades: 0, pmActiveTrades: 0, pmMonitoring: false, driftPositions: 0, unprotectedPositions: 0 } } } } /** * Start periodic health checks */ export function startPositionManagerHealthMonitor(): void { console.log('🏥 Starting Position Manager health monitor (every 30 seconds)...') // Initial check checkPositionManagerHealth().then(result => { logHealthCheckResult(result) }) // Periodic checks every 30 seconds setInterval(async () => { const result = await checkPositionManagerHealth() // Only log if there are issues or warnings if (!result.isHealthy || result.warnings.length > 0) { logHealthCheckResult(result) } }, 30000) // 30 seconds } function logHealthCheckResult(result: HealthCheckResult): void { if (result.isHealthy && result.warnings.length === 0) { console.log('✅ Position Manager health check PASSED') console.log(` DB: ${result.info.dbOpenTrades} open | PM: ${result.info.pmActiveTrades} active | Monitoring: ${result.info.pmMonitoring ? 'YES' : 'NO'} | Drift: ${result.info.driftPositions} positions`) return } console.log('\n🏥 POSITION MANAGER HEALTH CHECK REPORT:') console.log('━'.repeat(80)) if (result.issues.length > 0) { console.log('\n🔴 CRITICAL ISSUES:') result.issues.forEach(issue => console.log(issue)) } if (result.warnings.length > 0) { console.log('\n⚠️ WARNINGS:') result.warnings.forEach(warning => console.log(warning)) } console.log('\n📊 SYSTEM STATE:') console.log(` Database open trades: ${result.info.dbOpenTrades}`) console.log(` Position Manager active trades: ${result.info.pmActiveTrades}`) console.log(` Position Manager monitoring: ${result.info.pmMonitoring ? '✅ YES' : '❌ NO'}`) console.log(` Drift open positions: ${result.info.driftPositions}`) console.log(` Unprotected positions: ${result.info.unprotectedPositions}`) console.log('━'.repeat(80)) }