critical: Fix Smart Validation Queue blockReason mismatch (Bug #84)

Root Cause: check-risk endpoint passes blockReason='SMART_VALIDATION_QUEUED'
but addSignal() only accepted 'QUALITY_SCORE_TOO_LOW' → signals blocked but never queued

Impact: Quality 85 LONG signal at 08:40:03 saved to database but never monitored
User missed validation opportunity when price moved favorably

Fix: Accept both blockReason variants in addSignal() validation check

Evidence:
- Database record cmj41pdqu0101pf07mith5s4c has blockReason='SMART_VALIDATION_QUEUED'
- No logs showing addSignal() execution (would log ' Smart validation queued')
- check-risk code line 451 passes 'SMART_VALIDATION_QUEUED'
- addSignal() line 76 rejected signals != 'QUALITY_SCORE_TOO_LOW'

Result: Quality 50-89 signals will now be properly queued for validation
This commit is contained in:
mindesbunister
2025-12-13 17:24:38 +01:00
parent 12b4c7cafc
commit 5d5868d802
7 changed files with 575 additions and 69 deletions

View File

@@ -28,6 +28,65 @@ export interface HealthCheckResult {
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
}
}
/**
@@ -95,24 +154,42 @@ export async function checkPositionManagerHealth(): Promise<HealthCheckResult> {
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 (no SL/TP orders)
// 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) {
if (!trade.slOrderTx && !trade.softStopOrderTx && !trade.hardStopOrderTx) {
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) {
warnings.push(`⚠️ Position ${trade.symbol} missing TP1 order`)
if (!trade.tp1OrderTx && !isSyncedPosition) {
warnings.push(`⚠️ Position ${trade.symbol} missing TP1 order (not synced)`)
}
if (!trade.tp2OrderTx) {
warnings.push(`⚠️ Position ${trade.symbol} missing TP2 order`)
if (!trade.tp2OrderTx && !isSyncedPosition) {
warnings.push(`⚠️ Position ${trade.symbol} missing TP2 order (not synced)`)
}
}