Fix runner system by checking minimum position size viability

PROBLEM: Runner never activated because Drift force-closes positions below
minimum size. TP2 would close 80% leaving 5% runner (~$105), but Drift
automatically closed the entire position.

SOLUTION:
1. Created runner-calculator.ts with canUseRunner() to check if remaining
   size would be above Drift minimums BEFORE executing TP2 close
2. If runner not viable: Skip TP2 close entirely, activate trailing stop
   on full 25% remaining (from TP1)
3. If runner viable: Execute TP2 as normal, activate trailing on 5%

Benefits:
- Runner system will now actually work for viable position sizes
- Positions that are too small won't try to force-close below minimums
- Better logs showing why runner did/didn't activate
- Trailing stop works on larger % if runner not viable (better R:R)

Example: $2100 position → $525 after TP1 → $105 runner = VIABLE
         $4 ETH position → $1 after TP1 → $0.20 runner = NOT VIABLE

Runner will trail with ATR-based dynamic % (0.25-0.9%) below peak price.
This commit is contained in:
mindesbunister
2025-11-07 15:10:01 +01:00
parent 309cad8108
commit 36ba3809a1
3 changed files with 120 additions and 12 deletions

View File

@@ -500,19 +500,24 @@ export async function closePosition(
}
// Calculate size to close
let sizeToClose = position.size * (params.percentToClose / 100)
const sizeToClose = position.size * (params.percentToClose / 100)
const remainingSize = position.size - sizeToClose
// CRITICAL FIX: If calculated size is below minimum, close 100% instead
// This prevents "runner" positions from being too small to close
if (sizeToClose < marketConfig.minOrderSize) {
console.log(`⚠️ Calculated close size ${sizeToClose.toFixed(4)} is below minimum ${marketConfig.minOrderSize}`)
console.log(` Forcing 100% close to avoid Drift rejection`)
sizeToClose = position.size // Close entire position
// CRITICAL: Check if remaining position would be below Drift minimum
// If so, Drift will force-close the entire position anyway
// Better to detect this upfront and return fullyClosed=true
const willForceFullClose = remainingSize > 0 && remainingSize < marketConfig.minOrderSize
if (willForceFullClose && params.percentToClose < 100) {
console.log(`⚠️ WARNING: Remaining size ${remainingSize.toFixed(4)} would be below Drift minimum ${marketConfig.minOrderSize}`)
console.log(` Drift will force-close entire position. Proceeding with 100% close.`)
console.log(` 💡 TIP: Increase position size or decrease TP2 close % to enable runner`)
}
console.log(`📝 Close order details:`)
console.log(` Current position: ${position.size.toFixed(4)} ${position.side}`)
console.log(` Closing: ${params.percentToClose}% (${sizeToClose.toFixed(4)})`)
console.log(` Remaining after close: ${remainingSize.toFixed(4)}`)
console.log(` Entry price: $${position.entryPrice.toFixed(4)}`)
console.log(` Unrealized P&L: $${position.unrealizedPnL.toFixed(2)}`)
@@ -620,8 +625,8 @@ export async function closePosition(
// Check remaining position size after close
const updatedPosition = await driftService.getPosition(marketConfig.driftMarketIndex)
const remainingSize = updatedPosition ? Math.abs(updatedPosition.size) : 0
const fullyClosed = !updatedPosition || remainingSize === 0
const actualRemainingSize = updatedPosition ? Math.abs(updatedPosition.size) : 0
const fullyClosed = !updatedPosition || actualRemainingSize === 0 || willForceFullClose
if (fullyClosed) {
console.log('🗑️ Position fully closed, cancelling remaining orders...')
@@ -631,7 +636,7 @@ export async function closePosition(
}
} else if (params.percentToClose === 100) {
console.log(
`⚠️ Requested 100% close but ${remainingSize.toFixed(4)} base remains on-chain`
`⚠️ Requested 100% close but ${actualRemainingSize.toFixed(4)} base remains on-chain`
)
}
@@ -642,7 +647,7 @@ export async function closePosition(
closedSize: sizeToClose,
realizedPnL,
fullyClosed,
remainingSize,
remainingSize: actualRemainingSize,
}
} catch (error) {