/** * Trailing Stop Tests * * Tests for trailing stop functionality after TP2 trigger. * * Key behaviors tested: * - Trailing stop activates after TP2 trigger * - Calculate ATR-based trailing distance (1.5x ATR) * - Update peak price tracking as price moves favorably * - Trigger exit when price falls below trailing stop */ import { createLongTrade, createShortTrade, createTradeAfterTP2, TEST_DEFAULTS } from '../../helpers/trade-factory' describe('Trailing Stop', () => { // Extract trailing stop calculation logic from Position Manager function calculateTrailingStopPrice( peakPrice: number, trailingDistancePercent: number, direction: 'long' | 'short' ): number { if (direction === 'long') { return peakPrice * (1 - trailingDistancePercent / 100) } else { return peakPrice * (1 + trailingDistancePercent / 100) } } function calculateAtrBasedTrailingDistance( atr: number, currentPrice: number, multiplier: number, minPercent: number, maxPercent: number ): number { const atrPercent = (atr / currentPrice) * 100 const rawDistance = atrPercent * multiplier return Math.max(minPercent, Math.min(maxPercent, rawDistance)) } function shouldStopLoss(price: number, trade: { direction: 'long' | 'short', stopLossPrice: number }): boolean { if (trade.direction === 'long') { return price <= trade.stopLossPrice } else { return price >= trade.stopLossPrice } } describe('Trailing stop activation', () => { it('should activate trailing stop after TP2 trigger', () => { const trade = createTradeAfterTP2('long') expect(trade.tp2Hit).toBe(true) expect(trade.trailingStopActive).toBe(true) }) it('should NOT have trailing stop before TP2', () => { const trade = createLongTrade() expect(trade.tp2Hit).toBe(false) expect(trade.trailingStopActive).toBe(false) }) it('should NOT have trailing stop after TP1 only', () => { const trade = createLongTrade({ tp1Hit: true }) expect(trade.tp1Hit).toBe(true) expect(trade.tp2Hit).toBe(false) expect(trade.trailingStopActive).toBe(false) }) }) describe('ATR-based trailing distance calculation', () => { it('should calculate trailing distance as 1.5x ATR', () => { // ATR 0.43 at price $140 const atr = TEST_DEFAULTS.atr const currentPrice = TEST_DEFAULTS.entry const multiplier = 1.5 const minPercent = 0.25 const maxPercent = 0.9 const distance = calculateAtrBasedTrailingDistance( atr, currentPrice, multiplier, minPercent, maxPercent ) // 0.43 / 140 * 100 = 0.307% * 1.5 = 0.46% expect(distance).toBeCloseTo(0.46, 1) }) it('should clamp trailing distance to minimum', () => { // Very low ATR should clamp to min const atr = 0.1 const currentPrice = 140 const multiplier = 1.5 const minPercent = 0.25 const maxPercent = 0.9 const distance = calculateAtrBasedTrailingDistance( atr, currentPrice, multiplier, minPercent, maxPercent ) // 0.1 / 140 * 100 = 0.071% * 1.5 = 0.107% < min 0.25% expect(distance).toBe(minPercent) }) it('should clamp trailing distance to maximum', () => { // Very high ATR should clamp to max const atr = 2.0 const currentPrice = 140 const multiplier = 1.5 const minPercent = 0.25 const maxPercent = 0.9 const distance = calculateAtrBasedTrailingDistance( atr, currentPrice, multiplier, minPercent, maxPercent ) // 2.0 / 140 * 100 = 1.43% * 1.5 = 2.14% > max 0.9% expect(distance).toBe(maxPercent) }) }) describe('Peak price tracking', () => { it('LONG: should update peak price when price increases', () => { const trade = createTradeAfterTP2('long', { peakPrice: 142.41 }) const newHighPrice = 143.00 // Simulate peak price update logic if (newHighPrice > trade.peakPrice) { trade.peakPrice = newHighPrice } expect(trade.peakPrice).toBe(143.00) }) it('LONG: should NOT update peak price when price decreases', () => { const trade = createTradeAfterTP2('long', { peakPrice: 142.41 }) const lowerPrice = 142.00 // Simulate peak price update logic if (lowerPrice > trade.peakPrice) { trade.peakPrice = lowerPrice } expect(trade.peakPrice).toBe(142.41) // Unchanged }) it('SHORT: should update peak price when price decreases', () => { const trade = createTradeAfterTP2('short', { peakPrice: 137.59 }) const newLowPrice = 137.00 // Simulate peak price update logic (for short, lower is better) if (newLowPrice < trade.peakPrice) { trade.peakPrice = newLowPrice } expect(trade.peakPrice).toBe(137.00) }) it('SHORT: should NOT update peak price when price increases', () => { const trade = createTradeAfterTP2('short', { peakPrice: 137.59 }) const higherPrice = 138.00 // Simulate peak price update logic if (higherPrice < trade.peakPrice) { trade.peakPrice = higherPrice } expect(trade.peakPrice).toBe(137.59) // Unchanged }) }) describe('Trailing stop trigger', () => { it('LONG: should trigger when price falls below trailing SL', () => { const peakPrice = 143.00 const trailingPercent = 0.46 // ~0.46% trail const trailingSL = calculateTrailingStopPrice(peakPrice, trailingPercent, 'long') // Trail SL = 143 * (1 - 0.0046) = 142.34 expect(trailingSL).toBeCloseTo(142.34, 1) // Price above trail should not trigger expect(shouldStopLoss(142.50, { direction: 'long', stopLossPrice: trailingSL })).toBe(false) // Price at trail should trigger expect(shouldStopLoss(trailingSL, { direction: 'long', stopLossPrice: trailingSL })).toBe(true) // Price below trail should trigger expect(shouldStopLoss(142.00, { direction: 'long', stopLossPrice: trailingSL })).toBe(true) }) it('SHORT: should trigger when price rises above trailing SL', () => { const peakPrice = 137.00 const trailingPercent = 0.46 const trailingSL = calculateTrailingStopPrice(peakPrice, trailingPercent, 'short') // Trail SL = 137 * (1 + 0.0046) = 137.63 expect(trailingSL).toBeCloseTo(137.63, 1) // Price below trail should not trigger expect(shouldStopLoss(137.30, { direction: 'short', stopLossPrice: trailingSL })).toBe(false) // Price at trail should trigger expect(shouldStopLoss(trailingSL, { direction: 'short', stopLossPrice: trailingSL })).toBe(true) // Price above trail should trigger expect(shouldStopLoss(138.00, { direction: 'short', stopLossPrice: trailingSL })).toBe(true) }) it('LONG: trailing SL should move up but never down', () => { let currentTrailingSL = 142.00 const trailingPercent = 0.46 // Price rises to 143, new trail = 142.34 const newTrailingSL1 = calculateTrailingStopPrice(143.00, trailingPercent, 'long') if (newTrailingSL1 > currentTrailingSL) { currentTrailingSL = newTrailingSL1 } expect(currentTrailingSL).toBeCloseTo(142.34, 1) // Price drops to 142.50, trail should NOT move down const newTrailingSL2 = calculateTrailingStopPrice(142.50, trailingPercent, 'long') if (newTrailingSL2 > currentTrailingSL) { currentTrailingSL = newTrailingSL2 } expect(currentTrailingSL).toBeCloseTo(142.34, 1) // Unchanged // Price rises to 144, trail should move up const newTrailingSL3 = calculateTrailingStopPrice(144.00, trailingPercent, 'long') if (newTrailingSL3 > currentTrailingSL) { currentTrailingSL = newTrailingSL3 } expect(currentTrailingSL).toBeCloseTo(143.34, 1) // Moved up }) it('SHORT: trailing SL should move down but never up', () => { let currentTrailingSL = 138.00 const trailingPercent = 0.46 // Price drops to 137, new trail = 137.63 const newTrailingSL1 = calculateTrailingStopPrice(137.00, trailingPercent, 'short') if (newTrailingSL1 < currentTrailingSL) { currentTrailingSL = newTrailingSL1 } expect(currentTrailingSL).toBeCloseTo(137.63, 1) // Price rises to 137.50, trail should NOT move up const newTrailingSL2 = calculateTrailingStopPrice(137.50, trailingPercent, 'short') if (newTrailingSL2 < currentTrailingSL) { currentTrailingSL = newTrailingSL2 } expect(currentTrailingSL).toBeCloseTo(137.63, 1) // Unchanged // Price drops to 136, trail should move down const newTrailingSL3 = calculateTrailingStopPrice(136.00, trailingPercent, 'short') if (newTrailingSL3 < currentTrailingSL) { currentTrailingSL = newTrailingSL3 } expect(currentTrailingSL).toBeCloseTo(136.63, 1) // Moved down }) }) })