/** * Pure Runner with Profit-Based Widening Test * * Demonstrates TP2-as-runner strategy with dynamic trailing stop widening * based on profit level. * * Configuration: TAKE_PROFIT_2_SIZE_PERCENT=0 (pure runner) * * Validates: * - TP2 activates trailing stop without closing position * - Trailing stop widens as profit increases (>2% → 1.3x wider) * - Runner can capture large moves (e.g., 6%+ like user's chart) */ import { createLongTrade, TEST_DEFAULTS } from '../../helpers/trade-factory' describe('Pure Runner with Profit-Based Widening', () => { // Simulate Position Manager trailing stop calculation function calculateAdaptiveTrailingDistance( atr: number, currentPrice: number, profitPercent: number, baseMultiplier: number, minPercent: number, maxPercent: number ): number { const atrPercent = (atr / currentPrice) * 100 // Start with base multiplier let trailMultiplier = baseMultiplier // Profit-based widening (from position-manager.ts line 1562) if (profitPercent > 2.0) { trailMultiplier *= 1.3 // 30% wider at >2% profit } // Calculate distance const rawDistance = atrPercent * trailMultiplier // Clamp between min and max return Math.max(minPercent, Math.min(maxPercent, rawDistance)) } function calculateTrailingStopPrice( peakPrice: number, trailingDistancePercent: number, direction: 'long' | 'short' ): number { if (direction === 'long') { return peakPrice * (1 - trailingDistancePercent / 100) } else { return peakPrice * (1 + trailingDistancePercent / 100) } } describe('TP2 as pure runner trigger', () => { it('should activate trailing stop at TP2 without closing position', () => { const trade = createLongTrade({ entryPrice: 136.95, tp2Price: 140.13, // TP2 at +2.32% currentSize: 1984, // Full position remaining positionSize: 1984, }) // Simulate TP2 trigger with TAKE_PROFIT_2_SIZE_PERCENT=0 const tp2SizePercent = 0 // Pure runner config if (tp2SizePercent === 0) { trade.trailingStopActive = true trade.tp2Hit = true // Position NOT closed - full size remains expect(trade.currentSize).toBe(1984) } expect(trade.trailingStopActive).toBe(true) expect(trade.tp2Hit).toBe(true) expect(trade.currentSize).toBe(1984) // Full position still open }) }) describe('Profit-based trailing stop widening', () => { it('should use base trailing distance at low profit (<2%)', () => { const atr = TEST_DEFAULTS.atr // 0.43 const currentPrice = 140.00 const profitPercent = 1.5 // Below 2% threshold const baseMultiplier = 1.5 const minPercent = 0.25 const maxPercent = 0.9 const distance = calculateAdaptiveTrailingDistance( atr, currentPrice, profitPercent, baseMultiplier, minPercent, maxPercent ) // 0.43 / 140 * 100 = 0.307% * 1.5 = 0.46% expect(distance).toBeCloseTo(0.46, 1) }) it('should widen trailing distance at higher profit (>2%)', () => { const atr = TEST_DEFAULTS.atr // 0.43 const currentPrice = 143.00 // +4.4% from entry at 136.95 const profitPercent = 4.4 // Above 2% threshold const baseMultiplier = 1.5 const minPercent = 0.25 const maxPercent = 0.9 const distance = calculateAdaptiveTrailingDistance( atr, currentPrice, profitPercent, baseMultiplier, minPercent, maxPercent ) // 0.43 / 143 * 100 = 0.301% * 1.5 * 1.3 (profit multiplier) = 0.59% // 30% wider than base! expect(distance).toBeCloseTo(0.59, 1) }) it('should allow runner to capture 6%+ moves with wider trail', () => { const entryPrice = 136.95 const peakPrice = 145.00 // +5.9% move (like user's chart) const atr = 0.43 const profitPercent = ((peakPrice - entryPrice) / entryPrice) * 100 // Calculate trailing distance at high profit const baseMultiplier = 1.5 const trailDistance = calculateAdaptiveTrailingDistance( atr, peakPrice, profitPercent, baseMultiplier, 0.25, 0.9 ) // Trail at peak: 0.43 / 145 * 100 = 0.297% * 1.5 * 1.3 = 0.58% const trailingStopPrice = calculateTrailingStopPrice(peakPrice, trailDistance, 'long') // Trail SL = 145 * (1 - 0.0058) = 144.16 expect(trailingStopPrice).toBeCloseTo(144.16, 1) // Profit captured vs old system: // OLD: TP2 closed at $140.13 = +$18.56 // NEW: Runner closed at $144.16 = +$42.07 (2.3× more profit!) const oldProfit = (140.13 - entryPrice) / entryPrice * 100 const newProfit = (trailingStopPrice - entryPrice) / entryPrice * 100 expect(oldProfit).toBeCloseTo(2.32, 1) expect(newProfit).toBeCloseTo(5.27, 1) // 2.27× more profit }) }) describe('Real-world scenario: User\'s 6% move', () => { it('should capture most of 6% move with pure runner + profit widening', () => { // User's actual trade const entryPrice = 136.95 const tp1Close = 60 // 60% closed at TP1 const remainingRunner = 40 // 40% runner // Simulate price action const priceAction = [ { price: 140.13, profit: 2.32 }, // TP2 trigger (old: closed here) { price: 142.00, profit: 3.69 }, // +1.37% more { price: 144.00, profit: 5.15 }, // +2.83% more { price: 145.00, profit: 5.88 }, // +3.56% more (peak) ] const atr = 0.43 const baseMultiplier = 1.5 // Calculate where trailing stop would close const peakPrice = 145.00 const profitAtPeak = ((peakPrice - entryPrice) / entryPrice) * 100 const trailDistance = calculateAdaptiveTrailingDistance( atr, peakPrice, profitAtPeak, baseMultiplier, 0.25, 0.9 ) const trailingStopPrice = calculateTrailingStopPrice(peakPrice, trailDistance, 'long') // Runner exits around $144.16 (5.27% profit on runner) expect(trailingStopPrice).toBeGreaterThan(144.00) // Total P&L comparison: // OLD SYSTEM (TP2 close at $140.13): // TP1: 60% at +0.86% = +0.52% // TP2: 40% at +2.32% = +0.93% // Total: +1.45% of total position // NEW SYSTEM (Runner trails to ~$144.16): // TP1: 60% at +0.86% = +0.52% // Runner: 40% at +5.27% = +2.11% // Total: +2.63% of total position (1.8× better!) const oldSystemTotal = (0.6 * 0.86) + (0.4 * 2.32) const newSystemTotal = (0.6 * 0.86) + (0.4 * 5.27) expect(oldSystemTotal).toBeCloseTo(1.45, 1) expect(newSystemTotal).toBeCloseTo(2.63, 1) expect(newSystemTotal / oldSystemTotal).toBeGreaterThan(1.8) // 80% improvement! }) }) })