- 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>
272 lines
9.1 KiB
TypeScript
272 lines
9.1 KiB
TypeScript
/**
|
|
* 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
|
|
})
|
|
})
|
|
})
|