- 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>
156 lines
5.9 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|