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