Files
trading_bot_v4/tests/integration/position-manager/adx-runner-sl.test.ts
copilot-swe-agent[bot] 1b6297b1e2 chore: Fix .gitignore - remove test file exclusions, add coverage folder
- Removed incorrect exclusion of *.test.ts and *.test.js files
- Added coverage/ folder to .gitignore
- Removed accidentally committed coverage files

Co-authored-by: mindesbunister <32161838+mindesbunister@users.noreply.github.com>
2025-12-05 00:16:50 +00:00

201 lines
7.5 KiB
TypeScript

/**
* 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)
})
})
})