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

@@ -563,6 +563,29 @@ export async function closePosition(
error: 'No open position to close',
}
}
// BUG #89 FIX PART 2 (Dec 16, 2025): Pre-close validation for fractional positions
// Before attempting close, check if position is too small to close reliably
// Drift protocol has exchange-level constraints that prevent closing very small positions
// If position below minimum viable size, return error with manual resolution instructions
const oraclePriceForCheck = await driftService.getOraclePrice(marketConfig.driftMarketIndex)
const positionSizeUSD = Math.abs(position.size) * oraclePriceForCheck
const minViableSize = marketConfig.minOrderSize * 1.5
if (positionSizeUSD < minViableSize && params.percentToClose >= 100) {
console.log(`🛑 POSITION TOO SMALL TO CLOSE`)
console.log(` Position: ${Math.abs(position.size)} tokens ($${positionSizeUSD.toFixed(2)})`)
console.log(` Minimum viable size: $${minViableSize.toFixed(2)}`)
console.log(` This position is below Drift protocol's minimum viable close size`)
console.log(` Close transactions may confirm but leave fractional remnants`)
console.log(` Manual intervention required via Drift UI`)
return {
success: false,
error: 'POSITION_TOO_SMALL_TO_CLOSE',
closedSize: Math.abs(position.size),
} as any
}
logger.log('📊 Closing position:', params)
console.log(` params.percentToClose: ${params.percentToClose}`)