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.
This commit is contained in:
@@ -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<void> {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user