Files
trading_bot_v4/tests/integration/position-manager/trailing-stop.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

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