Files
trading_bot_v4/tests/integration/position-manager/breakeven-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

156 lines
5.9 KiB
TypeScript

/**
* Breakeven Stop Loss Tests
*
* Tests for SL movement to breakeven after TP1 hit.
*
* Key behaviors tested:
* - SL moves to entry price (breakeven) after TP1 for LONG
* - SL moves to entry price (breakeven) after TP1 for SHORT
* - CRITICAL (Pitfall #45): Must use DATABASE entry price, not Drift recalculated entry
*/
import {
createLongTrade,
createShortTrade,
createTradeAfterTP1,
TEST_DEFAULTS,
calculateTargetPrice
} from '../../helpers/trade-factory'
describe('Breakeven Stop Loss', () => {
// Test the calculatePrice logic extracted from Position Manager
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('LONG positions after TP1', () => {
it('should calculate breakeven SL at entry price for LONG', () => {
const trade = createLongTrade({ entryPrice: 140.00 })
// After TP1, SL moves to breakeven (0%)
const breakevenSL = calculatePrice(trade.entryPrice, 0, 'long')
expect(breakevenSL).toBe(140.00)
})
it('should protect 60% profit when runner SL at breakeven', () => {
// After TP1 closes 60%, remaining 40% has SL at entry
const trade = createTradeAfterTP1('long')
expect(trade.tp1Hit).toBe(true)
expect(trade.slMovedToBreakeven).toBe(true)
expect(trade.stopLossPrice).toBe(TEST_DEFAULTS.entry)
// Runner size should be 40% of original
expect(trade.currentSize).toBe(TEST_DEFAULTS.positionSize * 0.4)
})
it('should use DATABASE entry price, NOT Drift recalculated entry (Pitfall #45)', () => {
// CRITICAL: After partial close, Drift recalculates entry price based on remaining position
// This would give wrong breakeven SL. Must use ORIGINAL entry from database.
const databaseEntryPrice = 140.00
const driftRecalculatedEntry = 140.50 // Wrong! Drift adjusts after partial close
// Correct: Use database entry
const correctBreakevenSL = calculatePrice(databaseEntryPrice, 0, 'long')
expect(correctBreakevenSL).toBe(140.00)
// Wrong: Using Drift entry would give incorrect SL
const wrongBreakevenSL = calculatePrice(driftRecalculatedEntry, 0, 'long')
expect(wrongBreakevenSL).not.toBe(140.00)
expect(wrongBreakevenSL).toBe(140.50) // This would be wrong!
// The trade factory correctly uses original entry
const trade = createTradeAfterTP1('long', { entryPrice: databaseEntryPrice })
expect(trade.entryPrice).toBe(databaseEntryPrice)
expect(trade.stopLossPrice).toBe(databaseEntryPrice)
})
})
describe('SHORT positions after TP1', () => {
it('should calculate breakeven SL at entry price for SHORT', () => {
const trade = createShortTrade({ entryPrice: 140.00 })
// After TP1, SL moves to breakeven (0%)
const breakevenSL = calculatePrice(trade.entryPrice, 0, 'short')
expect(breakevenSL).toBe(140.00)
})
it('should protect 60% profit when runner SL at breakeven', () => {
const trade = createTradeAfterTP1('short')
expect(trade.tp1Hit).toBe(true)
expect(trade.slMovedToBreakeven).toBe(true)
expect(trade.stopLossPrice).toBe(TEST_DEFAULTS.entry)
// Runner size should be 40% of original
expect(trade.currentSize).toBe(TEST_DEFAULTS.positionSize * 0.4)
})
it('should use DATABASE entry price, NOT Drift recalculated entry (Pitfall #45)', () => {
const databaseEntryPrice = 140.00
const driftRecalculatedEntry = 139.50 // Wrong! Drift adjusts after partial close
// Correct: Use database entry
const correctBreakevenSL = calculatePrice(databaseEntryPrice, 0, 'short')
expect(correctBreakevenSL).toBe(140.00)
// Wrong: Using Drift entry would give incorrect SL
const wrongBreakevenSL = calculatePrice(driftRecalculatedEntry, 0, 'short')
expect(wrongBreakevenSL).not.toBe(140.00)
// The trade factory correctly uses original entry
const trade = createTradeAfterTP1('short', { entryPrice: databaseEntryPrice })
expect(trade.entryPrice).toBe(databaseEntryPrice)
expect(trade.stopLossPrice).toBe(databaseEntryPrice)
})
})
describe('SL direction verification', () => {
it('LONG: breakeven SL should be BELOW entry when negative %', () => {
const entryPrice = 140.00
// For LONG: negative % = lower price = valid SL
const slAt0_3PercentLoss = calculatePrice(entryPrice, -0.3, 'long')
expect(slAt0_3PercentLoss).toBe(139.58) // 140 * (1 - 0.003)
expect(slAt0_3PercentLoss).toBeLessThan(entryPrice)
})
it('SHORT: breakeven SL should be ABOVE entry when negative %', () => {
const entryPrice = 140.00
// For SHORT: negative % = higher price = valid SL
const slAt0_3PercentLoss = calculatePrice(entryPrice, -0.3, 'short')
expect(slAt0_3PercentLoss).toBe(140.42) // 140 * (1 + 0.003)
expect(slAt0_3PercentLoss).toBeGreaterThan(entryPrice)
})
it('should verify SL moves in profitable direction', () => {
const longTrade = createLongTrade({ entryPrice: 140 })
const shortTrade = createShortTrade({ entryPrice: 140 })
// Original SLs are at loss levels
expect(longTrade.stopLossPrice).toBe(TEST_DEFAULTS.long.sl) // Below entry
expect(shortTrade.stopLossPrice).toBe(TEST_DEFAULTS.short.sl) // Above entry
// After TP1, SLs move to breakeven (entry price)
const longAfterTP1 = createTradeAfterTP1('long')
const shortAfterTP1 = createTradeAfterTP1('short')
// Both should now be at entry price
expect(longAfterTP1.stopLossPrice).toBe(TEST_DEFAULTS.entry)
expect(shortAfterTP1.stopLossPrice).toBe(TEST_DEFAULTS.entry)
})
})
})