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.
75 lines
2.2 KiB
TypeScript
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)))
|
|
}
|