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:
@@ -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)`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user