/** * ADX-Based Runner Stop Loss Tests * * Tests for ADX-based runner SL positioning after TP1 (Pitfall #52). * * Runner SL tiers based on ADX at entry: * - ADX < 20: SL at 0% (breakeven) - Weak trend, preserve capital * - ADX 20-25: SL at -0.3% - Moderate trend, some room * - ADX > 25: SL at -0.55% - Strong trend, full retracement room */ import { createLongTrade, createShortTrade, TEST_DEFAULTS, calculateTargetPrice } from '../../helpers/trade-factory' describe('ADX-Based Runner Stop Loss', () => { // Extract the ADX-based runner SL logic from Position Manager function calculateRunnerSLPercent(adx: number): number { if (adx < 20) { return 0 // Weak trend: breakeven, preserve capital } else if (adx < 25) { return -0.3 // Moderate trend: some room } else { return -0.55 // Strong trend: full retracement room } } function calculatePrice( entryPrice: number, percent: number, direction: 'long' | 'short' ): number { if (direction === 'long') { return entryPrice * (1 + percent / 100) } else { return entryPrice * (1 - percent / 100) } } describe('ADX < 20: Weak trend - breakeven SL', () => { it('should return 0% SL for ADX 15 (weak trend)', () => { const runnerSlPercent = calculateRunnerSLPercent(15) expect(runnerSlPercent).toBe(0) }) it('should return 0% SL for ADX 19.9 (boundary)', () => { const runnerSlPercent = calculateRunnerSLPercent(19.9) expect(runnerSlPercent).toBe(0) }) it('LONG: should set runner SL at entry price for ADX 18', () => { const trade = createLongTrade({ adx: 18, entryPrice: 140 }) const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!) const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'long') expect(runnerSlPercent).toBe(0) expect(runnerSL).toBe(140.00) // Breakeven }) it('SHORT: should set runner SL at entry price for ADX 18', () => { const trade = createShortTrade({ adx: 18, entryPrice: 140 }) const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!) const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'short') expect(runnerSlPercent).toBe(0) expect(runnerSL).toBe(140.00) // Breakeven }) }) describe('ADX 20-25: Moderate trend - -0.3% SL', () => { it('should return -0.3% SL for ADX 20 (boundary)', () => { const runnerSlPercent = calculateRunnerSLPercent(20) expect(runnerSlPercent).toBe(-0.3) }) it('should return -0.3% SL for ADX 22', () => { const runnerSlPercent = calculateRunnerSLPercent(22) expect(runnerSlPercent).toBe(-0.3) }) it('should return -0.3% SL for ADX 24.9 (boundary)', () => { const runnerSlPercent = calculateRunnerSLPercent(24.9) expect(runnerSlPercent).toBe(-0.3) }) it('LONG: should set runner SL at -0.3% below entry for ADX 22', () => { const trade = createLongTrade({ adx: 22, entryPrice: 140 }) const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!) const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'long') expect(runnerSlPercent).toBe(-0.3) expect(runnerSL).toBeCloseTo(139.58, 2) // 140 * (1 - 0.003) = 139.58 expect(runnerSL).toBeLessThan(trade.entryPrice) }) it('SHORT: should set runner SL at -0.3% above entry for ADX 22', () => { const trade = createShortTrade({ adx: 22, entryPrice: 140 }) const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!) const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'short') expect(runnerSlPercent).toBe(-0.3) expect(runnerSL).toBeCloseTo(140.42, 2) // 140 * (1 + 0.003) = 140.42 expect(runnerSL).toBeGreaterThan(trade.entryPrice) }) }) describe('ADX > 25: Strong trend - -0.55% SL', () => { it('should return -0.55% SL for ADX 25 (boundary)', () => { const runnerSlPercent = calculateRunnerSLPercent(25) expect(runnerSlPercent).toBe(-0.55) }) it('should return -0.55% SL for ADX 26.9 (test default)', () => { const runnerSlPercent = calculateRunnerSLPercent(TEST_DEFAULTS.adx) expect(runnerSlPercent).toBe(-0.55) }) it('should return -0.55% SL for ADX 35 (very strong)', () => { const runnerSlPercent = calculateRunnerSLPercent(35) expect(runnerSlPercent).toBe(-0.55) }) it('LONG: should set runner SL at -0.55% below entry for ADX 26.9', () => { const trade = createLongTrade({ adx: 26.9, entryPrice: 140 }) const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!) const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'long') expect(runnerSlPercent).toBe(-0.55) expect(runnerSL).toBeCloseTo(139.23, 2) // 140 * (1 - 0.0055) = 139.23 expect(runnerSL).toBeLessThan(trade.entryPrice) }) it('SHORT: should set runner SL at -0.55% above entry for ADX 26.9', () => { const trade = createShortTrade({ adx: 26.9, entryPrice: 140 }) const runnerSlPercent = calculateRunnerSLPercent(trade.adxAtEntry!) const runnerSL = calculatePrice(trade.entryPrice, runnerSlPercent, 'short') expect(runnerSlPercent).toBe(-0.55) expect(runnerSL).toBeCloseTo(140.77, 2) // 140 * (1 + 0.0055) = 140.77 expect(runnerSL).toBeGreaterThan(trade.entryPrice) }) }) describe('Missing ADX handling', () => { it('should default to 0% (breakeven) when ADX is 0', () => { const runnerSlPercent = calculateRunnerSLPercent(0) expect(runnerSlPercent).toBe(0) // Conservative default }) it('should handle trades with no ADX data', () => { // When ADX is undefined, the Position Manager uses 0 as default // According to the logic: ADX < 20 = 0% SL (breakeven) const adx = undefined const adxValue = adx || 0 const runnerSlPercent = calculateRunnerSLPercent(adxValue) // No ADX = 0 = weak trend = breakeven expect(runnerSlPercent).toBe(0) }) }) describe('Retracement room validation', () => { it('LONG ADX 26.9: runner can handle -0.55% retracement without stop', () => { const entryPrice = 140 const runnerSL = calculatePrice(entryPrice, -0.55, 'long') // Price at -0.4% should NOT hit SL const priceAt0_4PercentDrop = entryPrice * 0.996 // 139.44 expect(priceAt0_4PercentDrop).toBeGreaterThan(runnerSL) // Price at -0.55% should hit SL const priceAt0_55PercentDrop = runnerSL expect(priceAt0_55PercentDrop).toBe(runnerSL) // Price at -0.6% should definitely hit SL const priceAt0_6PercentDrop = entryPrice * 0.994 // 139.16 expect(priceAt0_6PercentDrop).toBeLessThan(runnerSL) }) it('SHORT ADX 26.9: runner can handle +0.55% retracement without stop', () => { const entryPrice = 140 const runnerSL = calculatePrice(entryPrice, -0.55, 'short') // Price at +0.4% should NOT hit SL const priceAt0_4PercentRise = entryPrice * 1.004 // 140.56 expect(priceAt0_4PercentRise).toBeLessThan(runnerSL) // Price at +0.55% should hit SL const priceAt0_55PercentRise = runnerSL expect(priceAt0_55PercentRise).toBe(runnerSL) // Price at +0.6% should definitely hit SL const priceAt0_6PercentRise = entryPrice * 1.006 // 140.84 expect(priceAt0_6PercentRise).toBeGreaterThan(runnerSL) }) }) })