critical: Bug #89 - Detect and handle Drift fractional position remnants (3-part fix)

- Part 1: Position Manager fractional remnant detection after close attempts
  * Check if position < 1.5× minOrderSize after close transaction
  * Log to persistent logger with FRACTIONAL_REMNANT_DETECTED
  * Track closeAttempts, limit to 3 maximum
  * Mark exitReason='FRACTIONAL_REMNANT' in database
  * Remove from monitoring after 3 failed attempts

- Part 2: Pre-close validation in closePosition()
  * Check if position viable before attempting close
  * Reject positions < 1.5× minOrderSize with specific error
  * Prevent wasted transaction attempts on too-small positions
  * Return POSITION_TOO_SMALL_TO_CLOSE error with manual instructions

- Part 3: Health monitor detection for fractional remnants
  * Query Trade table for FRACTIONAL_REMNANT exits in last 24h
  * Alert operators with position details and manual cleanup instructions
  * Provide trade IDs, symbols, and Drift UI link

- Database schema: Added closeAttempts Int? field to Track attempts

Root cause: Drift protocol exchange constraints can leave fractional positions
Evidence: 3 close transactions confirmed but 0.15 SOL remnant persisted
Financial impact: ,000+ risk from unprotected fractional positions
Status: Fix implemented, awaiting deployment verification

See: docs/COMMON_PITFALLS.md Bug #89 for complete incident details
This commit is contained in:
mindesbunister
2025-12-16 22:05:12 +01:00
parent 7c8f1688aa
commit b11da009eb
6 changed files with 321 additions and 1 deletions

View File

@@ -1931,6 +1931,86 @@ export class PositionManager {
trade.realizedPnL += result.realizedPnL || 0
logger.log(`💰 P&L accumulated: +$${(result.realizedPnL || 0).toFixed(2)} | Total: $${trade.realizedPnL.toFixed(2)}`)
// BUG #89 FIX PART 1 (Dec 16, 2025): Detect fractional position remnants
// After close transaction confirms, verify Drift actually closed position
// If fractional remnant remains, log to persistent logger and mark for manual intervention
if (percentToClose >= 100) {
try {
const driftService = getDriftService()
const marketConfig = getMarketConfig(trade.symbol)
const position = await driftService.getPosition(marketConfig.driftMarketIndex)
if (position && Math.abs(position.size) > 0.01) {
// Position still exists after close - check if it's a fractional remnant
const positionSizeUSD = Math.abs(position.size) * currentPrice
const minViableSize = marketConfig.minOrderSize * 1.5
if (positionSizeUSD < minViableSize) {
console.log(`⚠️ FRACTIONAL REMNANT DETECTED: ${trade.symbol}`)
console.log(` Position: ${Math.abs(position.size)} tokens ($${positionSizeUSD.toFixed(2)})`)
console.log(` Below minimum viable size: $${minViableSize.toFixed(2)}`)
console.log(` Close transaction confirmed but remnant persisted`)
// Log to persistent logger for manual review
const { logCriticalError } = await import('../utils/persistent-logger')
await logCriticalError('FRACTIONAL_REMNANT_DETECTED', {
tradeId: trade.id,
symbol: trade.symbol,
remnantSize: Math.abs(position.size),
remnantUSD: positionSizeUSD,
minViableSize,
transactionSignature: result.transactionSignature,
closeAttempts: (trade as any).closeAttempts || 1,
})
// Track close attempts (initialize if undefined)
const closeAttempts = ((trade as any).closeAttempts || 0) + 1
;(trade as any).closeAttempts = closeAttempts
console.log(` Close attempts: ${closeAttempts}/3`)
if (closeAttempts >= 3) {
console.log(`🛑 FRACTIONAL REMNANT: Max attempts reached (3)`)
console.log(` Marking for manual intervention and removing from monitoring`)
console.log(` Manual cleanup required via Drift UI`)
// Mark trade with special exit reason for detection
try {
const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000)
await updateTradeExit({
positionId: trade.positionId,
exitPrice: result.closePrice || currentPrice,
exitReason: 'FRACTIONAL_REMNANT' as any,
realizedPnL: trade.realizedPnL,
exitOrderTx: result.transactionSignature || 'MANUAL_CLOSE',
holdTimeSeconds,
maxDrawdown: Math.abs(Math.min(0, trade.maxAdverseExcursion)),
maxGain: Math.max(0, trade.maxFavorableExcursion),
maxFavorableExcursion: trade.maxFavorableExcursion,
maxAdverseExcursion: trade.maxAdverseExcursion,
maxFavorablePrice: trade.maxFavorablePrice,
maxAdversePrice: trade.maxAdversePrice,
})
logger.log('💾 Fractional remnant marked in database')
} catch (dbError) {
console.error('❌ Failed to mark fractional remnant:', dbError)
}
// Remove from monitoring - manual intervention required
return
} else {
console.log(`⚠️ Will retry on next close attempt`)
// Keep monitoring - will retry if SL/TP triggers again
return
}
}
}
} catch (checkError) {
console.error('⚠️ Could not verify position after close:', checkError)
// Continue with normal flow - verification failure shouldn't block exit
}
}
// Update trade state
if (percentToClose >= 100) {
// Full close - remove from monitoring