Files
trading_bot_v4/lib/health/position-manager-health.ts
mindesbunister 2234bbc171 fix: Add getPrismaClient import for Bug #89 fractional remnant detection
- Fixed import in health monitor to include getPrismaClient
- Required for Bug #89 FRACTIONAL_REMNANT database queries
- Resolved Build 2 module resolution error (../database/client)
- Corrected import path to ../database/trades

Also includes v11.2 PineScript emergency parameter fix
2025-12-17 08:43:14 +01:00

311 lines
11 KiB
TypeScript
Raw Permalink 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.
This file contains Unicode characters that might be confused with other characters. 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, getPrismaClient } 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
}
autoSyncTriggered?: boolean
}
// Cooldown to prevent sync spam (5 minutes)
let lastAutoSyncTime = 0
const AUTO_SYNC_COOLDOWN_MS = 5 * 60 * 1000
/**
* Automatically trigger position sync when untracked positions detected
*
* CRITICAL: This prevents the $1,000 loss scenario where Telegram bot
* opens positions but Position Manager never tracks them.
*
* Cooldown: 5 minutes between auto-syncs to prevent spam
*/
async function autoSyncUntrackedPositions(): Promise<boolean> {
const now = Date.now()
// Check cooldown
if (now - lastAutoSyncTime < AUTO_SYNC_COOLDOWN_MS) {
const remainingSeconds = Math.ceil((AUTO_SYNC_COOLDOWN_MS - (now - lastAutoSyncTime)) / 1000)
console.log(`⏳ Auto-sync cooldown active (${remainingSeconds}s remaining)`)
return false
}
try {
console.log('🔄 AUTO-SYNC TRIGGERED: Untracked positions detected, syncing with Drift...')
// Call the sync endpoint internally
const pm = await getInitializedPositionManager()
const driftService = getDriftService()
const driftPositions = await driftService.getAllPositions()
console.log(`📊 Found ${driftPositions.length} positions on Drift`)
let added = 0
for (const driftPos of driftPositions) {
const isTracked = Array.from((pm as any).activeTrades.values()).some(
(t: any) => t.symbol === driftPos.symbol
)
if (!isTracked && Math.abs(driftPos.size) > 0) {
console.log(` Auto-adding ${driftPos.symbol} to Position Manager`)
// Import sync logic
const { syncSinglePosition } = await import('../trading/sync-helper')
await syncSinglePosition(driftPos, pm)
added++
}
}
lastAutoSyncTime = now
console.log(`✅ AUTO-SYNC COMPLETE: Added ${added} position(s) to monitoring`)
return true
} catch (error) {
console.error('❌ Auto-sync failed:', error)
return false
}
}
/**
* 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`)
// AUTO-SYNC: If Drift has MORE positions than PM, sync automatically
if (driftPositions > pmActiveTrades) {
console.log(`🚨 UNTRACKED POSITIONS DETECTED: Drift has ${driftPositions}, PM has ${pmActiveTrades}`)
const synced = await autoSyncUntrackedPositions()
if (synced) {
warnings.push(`✅ AUTO-SYNC: Triggered position sync to restore protection`)
// Re-check PM state after sync
pmActiveTrades = (pm as any).activeTrades?.size || 0
}
}
}
// Check for unprotected positions
// NOTE: Synced/placeholder positions (signalSource='autosync') have NULL signatures in DB
// but orders exist on Drift. Position Manager monitoring provides backup protection.
let unprotectedPositions = 0
for (const trade of dbTrades) {
const hasDbSignatures = !!(trade.slOrderTx || trade.softStopOrderTx || trade.hardStopOrderTx)
const isSyncedPosition = trade.signalSource === 'autosync' || trade.timeframe === 'sync'
if (!hasDbSignatures && !isSyncedPosition) {
// This is NOT a synced position but has no SL orders - CRITICAL
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 && !isSyncedPosition) {
warnings.push(`⚠️ Position ${trade.symbol} missing TP1 order (not synced)`)
}
if (!trade.tp2OrderTx && !isSyncedPosition) {
warnings.push(`⚠️ Position ${trade.symbol} missing TP2 order (not synced)`)
}
}
// BUG #89 FIX PART 3 (Dec 16, 2025): Check for fractional position remnants
// Query database for positions marked as FRACTIONAL_REMNANT in last 24 hours
// These require manual intervention via Drift UI
const prisma = getPrismaClient()
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
const fractionalRemnants = await prisma.trade.findMany({
where: {
exitReason: 'FRACTIONAL_REMNANT' as any,
exitTime: { gte: twentyFourHoursAgo }
},
select: {
id: true,
symbol: true,
positionSizeUSD: true,
exitTime: true
}
})
if (fractionalRemnants.length > 0) {
issues.push(`🛑 CRITICAL: ${fractionalRemnants.length} fractional position remnant(s) detected!`)
issues.push(` These are positions that confirmed close but left small remnants on Drift`)
issues.push(` Close attempts exhausted (3 max) - MANUAL INTERVENTION REQUIRED`)
for (const remnant of fractionalRemnants) {
issues.push(`${remnant.symbol}: Size $${remnant.positionSizeUSD.toFixed(2)} (Trade ID: ${remnant.id})`)
issues.push(` Detected at: ${remnant.exitTime?.toISOString()}`)
issues.push(` Action: Close manually via Drift UI at https://app.drift.trade`)
}
}
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))
}