Files
trading_bot_v4/lib/health/position-manager-health.ts
mindesbunister d637aac2d7 feat: Deploy HA auto-failover with database promotion
- Enhanced DNS failover monitor on secondary (72.62.39.24)
- Auto-promotes database: pg_ctl promote on failover
- Creates DEMOTED flag on primary via SSH (split-brain protection)
- Telegram notifications with database promotion status
- Startup safety script ready (integration pending)
- 90-second automatic recovery vs 10-30 min manual
- Zero-cost 95% enterprise HA benefit

Status: DEPLOYED and MONITORING (14:52 CET)
Next: Controlled failover test during maintenance
2025-12-12 15:54:03 +01:00

203 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<HealthCheckResult> {
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)
let pmActiveTrades = pmState.activeTrades?.size || 0
let 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) {
console.log('🛠️ Health monitor: Attempting automatic monitoring restore from DB...')
try {
await pm.initialize(true)
pmActiveTrades = (pm as any).activeTrades?.size || 0
pmMonitoring = (pm as any).isMonitoring || false
} catch (restoreError) {
console.error('❌ Failed to auto-restore monitoring:', restoreError)
}
}
// Re-check after attempted restore
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))
}