Files
trading_bot_v4/lib/trading/runner-calculator.ts
mindesbunister 36ba3809a1 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.
2025-11-07 15:10:01 +01:00

75 lines
2.2 KiB
TypeScript

/**
* Calculate if runner system is viable for current position size
*
* Runner needs to be above Drift minimum order size after TP1 and TP2 close
*/
import { getMarketConfig } from '../../config/trading'
export function canUseRunner(
symbol: string,
positionSizeUSD: number,
currentPrice: number,
tp1Percent: number = 75,
tp2Percent: number = 80
): { viable: boolean; runnerSizeUSD: number; runnerSizeBase: number; reason?: string } {
const marketConfig = getMarketConfig(symbol)
// Calculate runner size in USD
const afterTp1 = positionSizeUSD * ((100 - tp1Percent) / 100) // 25% remaining
const runnerSizeUSD = afterTp1 * ((100 - tp2Percent) / 100) // 20% of that = 5% total
// Convert to base asset size
const runnerSizeBase = runnerSizeUSD / currentPrice
// Check if above minimum
const minRequired = marketConfig.minOrderSize
const viable = runnerSizeBase >= minRequired
if (!viable) {
return {
viable: false,
runnerSizeUSD,
runnerSizeBase,
reason: `Runner ${runnerSizeBase.toFixed(4)} < minimum ${minRequired} ${symbol.replace('-PERP', '')}`
}
}
return {
viable: true,
runnerSizeUSD,
runnerSizeBase
}
}
/**
* Calculate maximum TP2 close percent that leaves a viable runner
* Returns adjusted TP2% that ensures runner >= minimum size
*/
export function getViableTP2Percent(
symbol: string,
positionSizeUSD: number,
currentPrice: number,
tp1Percent: number = 75
): number {
const marketConfig = getMarketConfig(symbol)
const afterTp1SizeUSD = positionSizeUSD * ((100 - tp1Percent) / 100)
// Calculate minimum runner size we need in USD
const minRunnerBase = marketConfig.minOrderSize * 1.1 // Add 10% buffer
const minRunnerUSD = minRunnerBase * currentPrice
// Runner = afterTp1 * (100 - tp2%) / 100
// minRunnerUSD = afterTp1 * (100 - tp2%) / 100
// Solve for tp2%:
// (100 - tp2%) = (minRunnerUSD / afterTp1) * 100
// tp2% = 100 - ((minRunnerUSD / afterTp1) * 100)
const maxTP2Percent = 100 - ((minRunnerUSD / afterTp1SizeUSD) * 100)
// Clamp between 0-95% (never close more than 95% at TP2)
return Math.max(0, Math.min(95, Math.floor(maxTP2Percent)))
}