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