From 9db5f8566de0f918b72c146e73c44464939b502a Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Sun, 16 Nov 2025 00:22:19 +0100 Subject: [PATCH] refactor: Remove time-based ghost detection, rely purely on Drift API User feedback: Time-based cleanup (6 hours) too aggressive for legitimate long-running positions. Drift API is the authoritative source of truth. Changes: - Removed cleanupStalePositions() method entirely - Removed age-based Layer 1 from validatePositions() - Updated Layer 2: Now verifies with Drift API before removing position - All ghost detection now uses Drift blockchain as source of truth Ghost detection methods: - Layer 2: Queries Drift after 20 failed close attempts - Layer 3: Queries Drift every 40 seconds during monitoring - Periodic validation: Queries Drift every 5 minutes Result: No premature closures, more reliable ghost detection. --- lib/trading/position-manager.ts | 59 ++++++++++++--------------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index df21784..b684d07 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -237,18 +237,15 @@ export class PositionManager { console.log('🔍 Validating positions against Drift...') - // LAYER 1: Database-based age check (doesn't require RPC - always works) - await this.cleanupStalePositions() - try { const driftService = getDriftService() - // If Drift service not ready, use database-only validation + // If Drift service not ready, skip this validation cycle if (!driftService || !(driftService as any).isInitialized) { - console.log('⚠️ Drift service not ready - using database-only validation') + console.log('⚠️ Drift service not ready - skipping validation this cycle') console.log(` Positions in memory: ${this.activeTrades.size}`) - console.log(` These will be checked against database on next monitoring cycle`) - return // Database cleanup already ran above + console.log(` Will retry on next cycle (5 minutes) or during monitoring (40 seconds)`) + return } // Check each tracked trade individually @@ -283,30 +280,6 @@ export class PositionManager { } } - /** - * LAYER 1: Database-based stale position cleanup - * - * Removes positions from memory that are older than 6 hours - * Doesn't require RPC calls - always works even during rate limiting - * - * CRITICAL: This prevents ghost accumulation during rate limit death spirals - */ - private async cleanupStalePositions(): Promise { - const sixHoursAgo = Date.now() - (6 * 60 * 60 * 1000) - - for (const [tradeId, trade] of this.activeTrades) { - // If position is >6 hours old, it's likely a ghost (max trade duration should be ~2-3 hours) - if (trade.entryTime < sixHoursAgo) { - console.log(`🔴 STALE GHOST DETECTED: ${trade.symbol} (age: ${Math.floor((Date.now() - trade.entryTime) / 3600000)}h)`) - console.log(` Entry time: ${new Date(trade.entryTime).toISOString()}`) - console.log(` Removing from memory - likely closed externally hours ago`) - - await this.handleExternalClosure(trade, 'Stale position cleanup (>6h old)') - console.log(`✅ Stale ghost cleaned up: ${trade.symbol}`) - } - } - } - /** * Handle external closure for ghost position cleanup * @@ -1178,13 +1151,25 @@ export class PositionManager { console.error(`⚠️ Rate limited while closing ${trade.symbol} - will retry on next price update`) // LAYER 2: Death spiral detector (Nov 15, 2025) - // If we've failed to close this position 20+ times (40+ seconds of retries), - // force remove from monitoring to prevent infinite rate limit storms + // If we've failed 20+ times, check Drift API to see if it's a ghost position if (trade.priceCheckCount > 20) { - console.log(`🔴 DEATH SPIRAL DETECTED: ${trade.symbol} failed 20+ close attempts`) - console.log(` Forcing removal from monitoring to prevent rate limit exhaustion`) - await this.handleExternalClosure(trade, 'Death spiral prevention (20+ failed close attempts)') - return + try { + const driftService = getDriftService() + const marketConfig = getMarketConfig(trade.symbol) + const position = await driftService.getPosition(marketConfig.driftMarketIndex) + + // If position doesn't exist on Drift, it's a ghost - remove immediately + if (!position || Math.abs(position.size) < 0.01) { + console.log(`🔴 LAYER 2: Ghost detected after ${trade.priceCheckCount} failures`) + console.log(` Drift shows position closed/missing - removing from monitoring`) + await this.handleExternalClosure(trade, 'Layer 2: Ghost detected via Drift API') + return + } else { + console.log(` Position verified on Drift (size: ${position.size}) - will keep retrying`) + } + } catch (checkError) { + console.error(` Could not verify position on Drift:`, checkError) + } } // DON'T remove trade from monitoring - let it retry naturally