- 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>
201 lines
7.5 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|