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:
155
tests/integration/position-manager/breakeven-sl.test.ts
Normal file
155
tests/integration/position-manager/breakeven-sl.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user