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>
This commit is contained in:
271
tests/integration/position-manager/trailing-stop.test.ts
Normal file
271
tests/integration/position-manager/trailing-stop.test.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* 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
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user