- 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>
198 lines
7.2 KiB
TypeScript
198 lines
7.2 KiB
TypeScript
/**
|
|
* Price Verification Tests
|
|
*
|
|
* Tests for price verification before setting TP flags.
|
|
*
|
|
* Key behaviors tested (Pitfall #43):
|
|
* - Verify price reached TP1 before setting tp1Hit flag
|
|
* - Require BOTH size reduced AND price at target
|
|
* - Use tolerance for price target matching (0.2%)
|
|
*/
|
|
|
|
import {
|
|
createLongTrade,
|
|
createShortTrade,
|
|
TEST_DEFAULTS
|
|
} from '../../helpers/trade-factory'
|
|
|
|
describe('Price Verification', () => {
|
|
// Extract isPriceAtTarget logic from Position Manager
|
|
function isPriceAtTarget(currentPrice: number, targetPrice: number, tolerance: number = 0.002): boolean {
|
|
if (!targetPrice || targetPrice === 0) return false
|
|
const diff = Math.abs(currentPrice - targetPrice) / targetPrice
|
|
return diff <= tolerance
|
|
}
|
|
|
|
function shouldTakeProfit1(price: number, trade: { direction: 'long' | 'short', tp1Price: number }): boolean {
|
|
if (trade.direction === 'long') {
|
|
return price >= trade.tp1Price
|
|
} else {
|
|
return price <= trade.tp1Price
|
|
}
|
|
}
|
|
|
|
describe('TP1 verification before setting flag (Pitfall #43)', () => {
|
|
it('LONG: should require BOTH size reduction AND price at TP1', () => {
|
|
const trade = createLongTrade({ entryPrice: 140, tp1Price: 141.20 })
|
|
|
|
// Scenario: Size reduced but price NOT at TP1
|
|
const sizeReduced = true
|
|
const currentPrice = 140.50 // Below TP1
|
|
const priceAtTP1 = isPriceAtTarget(currentPrice, trade.tp1Price)
|
|
|
|
// Should NOT set tp1Hit because price hasn't reached target
|
|
const shouldSetTP1 = sizeReduced && priceAtTP1
|
|
expect(shouldSetTP1).toBe(false)
|
|
|
|
// This was likely a MANUAL CLOSE, not TP1
|
|
})
|
|
|
|
it('LONG: should allow TP1 when both conditions met', () => {
|
|
const trade = createLongTrade({ entryPrice: 140, tp1Price: 141.20 })
|
|
|
|
// Scenario: Size reduced AND price at TP1
|
|
const sizeReduced = true
|
|
const currentPrice = 141.20
|
|
const priceAtTP1 = isPriceAtTarget(currentPrice, trade.tp1Price)
|
|
|
|
const shouldSetTP1 = sizeReduced && priceAtTP1
|
|
expect(shouldSetTP1).toBe(true)
|
|
})
|
|
|
|
it('SHORT: should require BOTH size reduction AND price at TP1', () => {
|
|
const trade = createShortTrade({ entryPrice: 140, tp1Price: 138.80 })
|
|
|
|
// Scenario: Size reduced but price NOT at TP1
|
|
const sizeReduced = true
|
|
const currentPrice = 139.50 // Above TP1 (short profits when price drops)
|
|
const priceAtTP1 = isPriceAtTarget(currentPrice, trade.tp1Price)
|
|
|
|
const shouldSetTP1 = sizeReduced && priceAtTP1
|
|
expect(shouldSetTP1).toBe(false)
|
|
})
|
|
|
|
it('SHORT: should allow TP1 when both conditions met', () => {
|
|
const trade = createShortTrade({ entryPrice: 140, tp1Price: 138.80 })
|
|
|
|
const sizeReduced = true
|
|
const currentPrice = 138.80
|
|
const priceAtTP1 = isPriceAtTarget(currentPrice, trade.tp1Price)
|
|
|
|
const shouldSetTP1 = sizeReduced && priceAtTP1
|
|
expect(shouldSetTP1).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('isPriceAtTarget tolerance (0.2%)', () => {
|
|
it('should return true when price exactly at target', () => {
|
|
const result = isPriceAtTarget(141.20, 141.20)
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('should return true when price within 0.2% of target', () => {
|
|
// 0.2% of 141.20 = 0.2824
|
|
// So prices from 140.92 to 141.48 should be "at target"
|
|
|
|
expect(isPriceAtTarget(141.10, 141.20)).toBe(true) // -0.07%
|
|
expect(isPriceAtTarget(141.30, 141.20)).toBe(true) // +0.07%
|
|
expect(isPriceAtTarget(141.00, 141.20)).toBe(true) // -0.14%
|
|
expect(isPriceAtTarget(141.40, 141.20)).toBe(true) // +0.14%
|
|
})
|
|
|
|
it('should return false when price outside 0.2% tolerance', () => {
|
|
// More than 0.2% away should NOT be "at target"
|
|
expect(isPriceAtTarget(140.70, 141.20)).toBe(false) // -0.35%
|
|
expect(isPriceAtTarget(141.60, 141.20)).toBe(false) // +0.28%
|
|
expect(isPriceAtTarget(140.00, 141.20)).toBe(false) // -0.85%
|
|
})
|
|
|
|
it('should handle edge case of 0 target price', () => {
|
|
const result = isPriceAtTarget(141.20, 0)
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('should handle custom tolerance', () => {
|
|
// Use stricter 0.1% tolerance
|
|
expect(isPriceAtTarget(141.10, 141.20, 0.001)).toBe(true) // 0.07% < 0.1%
|
|
expect(isPriceAtTarget(141.00, 141.20, 0.001)).toBe(false) // 0.14% > 0.1%
|
|
|
|
// Use looser 0.5% tolerance
|
|
expect(isPriceAtTarget(140.50, 141.20, 0.005)).toBe(true) // 0.5% = 0.5%
|
|
expect(isPriceAtTarget(140.00, 141.20, 0.005)).toBe(false) // 0.85% > 0.5%
|
|
})
|
|
})
|
|
|
|
describe('shouldTakeProfit1 with price verification', () => {
|
|
it('LONG: combined size + price verification', () => {
|
|
const trade = createLongTrade({
|
|
entryPrice: 140,
|
|
tp1Price: 141.20,
|
|
positionSize: 8000
|
|
})
|
|
|
|
// Simulate Position Manager logic
|
|
const originalSize = trade.positionSize
|
|
const currentSize = 3200 // After 60% close
|
|
const sizeMismatch = currentSize < originalSize * 0.9
|
|
|
|
// Case 1: Price at TP1
|
|
const priceAtTP1 = 141.20
|
|
const priceReachedTP1 = shouldTakeProfit1(priceAtTP1, trade)
|
|
expect(sizeMismatch && priceReachedTP1).toBe(true) // Valid TP1
|
|
|
|
// Case 2: Price NOT at TP1
|
|
const priceNotAtTP1 = 140.50
|
|
const priceReachedTP1_2 = shouldTakeProfit1(priceNotAtTP1, trade)
|
|
expect(sizeMismatch && priceReachedTP1_2).toBe(false) // Invalid - manual close
|
|
})
|
|
|
|
it('SHORT: combined size + price verification', () => {
|
|
const trade = createShortTrade({
|
|
entryPrice: 140,
|
|
tp1Price: 138.80,
|
|
positionSize: 8000
|
|
})
|
|
|
|
const originalSize = trade.positionSize
|
|
const currentSize = 3200
|
|
const sizeMismatch = currentSize < originalSize * 0.9
|
|
|
|
// Case 1: Price at TP1 (below entry for short)
|
|
const priceAtTP1 = 138.80
|
|
const priceReachedTP1 = shouldTakeProfit1(priceAtTP1, trade)
|
|
expect(sizeMismatch && priceReachedTP1).toBe(true)
|
|
|
|
// Case 2: Price NOT at TP1 (still above target)
|
|
const priceNotAtTP1 = 139.50
|
|
const priceReachedTP1_2 = shouldTakeProfit1(priceNotAtTP1, trade)
|
|
expect(sizeMismatch && priceReachedTP1_2).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('TP2 price verification', () => {
|
|
function shouldTakeProfit2(price: number, trade: { direction: 'long' | 'short', tp2Price: number }): boolean {
|
|
if (trade.direction === 'long') {
|
|
return price >= trade.tp2Price
|
|
} else {
|
|
return price <= trade.tp2Price
|
|
}
|
|
}
|
|
|
|
it('LONG: should detect TP2 at correct price', () => {
|
|
const trade = createLongTrade({ tp2Price: 142.41 })
|
|
|
|
expect(shouldTakeProfit2(142.00, trade)).toBe(false) // Below
|
|
expect(shouldTakeProfit2(142.41, trade)).toBe(true) // At target
|
|
expect(shouldTakeProfit2(143.00, trade)).toBe(true) // Above
|
|
})
|
|
|
|
it('SHORT: should detect TP2 at correct price', () => {
|
|
const trade = createShortTrade({ tp2Price: 137.59 })
|
|
|
|
expect(shouldTakeProfit2(138.00, trade)).toBe(false) // Above (bad for short)
|
|
expect(shouldTakeProfit2(137.59, trade)).toBe(true) // At target
|
|
expect(shouldTakeProfit2(137.00, trade)).toBe(true) // Below (better for short)
|
|
})
|
|
})
|
|
})
|