/** * Trade Factory - Test Helpers * * Factory functions to create mock trades for Position Manager testing. * Uses realistic values based on actual trading data documented in: * - Problem statement test data * - Common Pitfalls documentation */ import { ActiveTrade } from '../../lib/trading/position-manager' /** * Default test values based on problem statement: * - LONG: entry $140, TP1 $141.20 (+0.86%), TP2 $142.41 (+1.72%), SL $138.71 (-0.92%) * - SHORT: entry $140, TP1 $138.80 (-0.86%), TP2 $137.59 (-1.72%), SL $141.29 (+0.92%) * - ATR: 0.43, ADX: 26.9, Quality Score: 95, Position Size: $8000 */ export const TEST_DEFAULTS = { entry: 140.00, atr: 0.43, adx: 26.9, qualityScore: 95, positionSize: 8000, leverage: 15, // Calculated targets for LONG (entry + %) long: { tp1: 141.20, // +0.86% tp2: 142.41, // +1.72% sl: 138.71, // -0.92% emergencySl: 137.20, // -2% }, // Calculated targets for SHORT (entry - %) short: { tp1: 138.80, // -0.86% tp2: 137.59, // -1.72% sl: 141.29, // +0.92% emergencySl: 142.80, // +2% }, } /** * Options for creating a mock trade */ export interface CreateMockTradeOptions { id?: string positionId?: string symbol?: string direction?: 'long' | 'short' entryPrice?: number positionSize?: number leverage?: number atr?: number adx?: number qualityScore?: number // Targets tp1Price?: number tp2Price?: number stopLossPrice?: number emergencyStopPrice?: number // State overrides currentSize?: number tp1Hit?: boolean tp2Hit?: boolean slMovedToBreakeven?: boolean slMovedToProfit?: boolean trailingStopActive?: boolean peakPrice?: number // P&L tracking realizedPnL?: number unrealizedPnL?: number maxFavorableExcursion?: number maxAdverseExcursion?: number } /** * Generate a unique trade ID for testing */ let tradeCounter = 0 export function generateTradeId(): string { return `test-trade-${++tradeCounter}-${Date.now()}` } /** * Create a mock ActiveTrade object with sensible defaults */ export function createMockTrade(options: CreateMockTradeOptions = {}): ActiveTrade { const direction = options.direction || 'long' const entryPrice = options.entryPrice || TEST_DEFAULTS.entry const positionSize = options.positionSize || TEST_DEFAULTS.positionSize const targets = direction === 'long' ? TEST_DEFAULTS.long : TEST_DEFAULTS.short return { id: options.id || generateTradeId(), positionId: options.positionId || `tx-${Date.now()}`, symbol: options.symbol || 'SOL-PERP', direction, // Entry details entryPrice, entryTime: Date.now() - 60000, // Started 1 minute ago positionSize, leverage: options.leverage || TEST_DEFAULTS.leverage, atrAtEntry: options.atr ?? TEST_DEFAULTS.atr, adxAtEntry: options.adx ?? TEST_DEFAULTS.adx, signalQualityScore: options.qualityScore ?? TEST_DEFAULTS.qualityScore, signalSource: 'tradingview', // Targets - use provided or calculate from direction stopLossPrice: options.stopLossPrice ?? targets.sl, tp1Price: options.tp1Price ?? targets.tp1, tp2Price: options.tp2Price ?? targets.tp2, emergencyStopPrice: options.emergencyStopPrice ?? targets.emergencySl, // State currentSize: options.currentSize ?? positionSize, originalPositionSize: positionSize, takeProfitPrice1: options.tp1Price ?? targets.tp1, takeProfitPrice2: options.tp2Price ?? targets.tp2, tp1Hit: options.tp1Hit ?? false, tp2Hit: options.tp2Hit ?? false, slMovedToBreakeven: options.slMovedToBreakeven ?? false, slMovedToProfit: options.slMovedToProfit ?? false, trailingStopActive: options.trailingStopActive ?? false, // P&L tracking realizedPnL: options.realizedPnL ?? 0, unrealizedPnL: options.unrealizedPnL ?? 0, peakPnL: 0, peakPrice: options.peakPrice ?? entryPrice, // MAE/MFE tracking (as percentages per Pitfall #54) maxFavorableExcursion: options.maxFavorableExcursion ?? 0, maxAdverseExcursion: options.maxAdverseExcursion ?? 0, maxFavorablePrice: entryPrice, maxAdversePrice: entryPrice, // Monitoring priceCheckCount: 0, lastPrice: entryPrice, lastUpdateTime: Date.now(), } } /** * Create a LONG position with standard test defaults */ export function createLongTrade(options: Omit = {}): ActiveTrade { return createMockTrade({ ...options, direction: 'long', }) } /** * Create a SHORT position with standard test defaults */ export function createShortTrade(options: Omit = {}): ActiveTrade { return createMockTrade({ ...options, direction: 'short', }) } /** * Create a trade that has already hit TP1 */ export function createTradeAfterTP1( direction: 'long' | 'short', options: Omit = {} ): ActiveTrade { const positionSize = options.positionSize || TEST_DEFAULTS.positionSize const tp1SizePercent = 60 // Default TP1 closes 60% const remainingSize = positionSize * ((100 - tp1SizePercent) / 100) return createMockTrade({ ...options, direction, tp1Hit: true, slMovedToBreakeven: true, currentSize: remainingSize, // SL moves to entry (breakeven) after TP1 for weak ADX, or adjusted for strong ADX stopLossPrice: options.entryPrice || TEST_DEFAULTS.entry, }) } /** * Create a trade that has hit TP2 with trailing stop active */ export function createTradeAfterTP2( direction: 'long' | 'short', options: Omit = {} ): ActiveTrade { const positionSize = options.positionSize || TEST_DEFAULTS.positionSize const tp1SizePercent = 60 const tp2SizePercent = 0 // TP2 as runner - no close at TP2 const remainingSize = positionSize * ((100 - tp1SizePercent) / 100) const entryPrice = options.entryPrice || TEST_DEFAULTS.entry const targets = direction === 'long' ? TEST_DEFAULTS.long : TEST_DEFAULTS.short return createMockTrade({ ...options, direction, tp1Hit: true, tp2Hit: true, slMovedToBreakeven: true, trailingStopActive: true, currentSize: remainingSize, peakPrice: targets.tp2, // Peak is at TP2 level stopLossPrice: entryPrice, // Breakeven as starting point for trailing }) } /** * Helper to calculate expected profit percent */ export function calculateExpectedProfitPercent( entryPrice: number, currentPrice: number, direction: 'long' | 'short' ): number { if (direction === 'long') { return ((currentPrice - entryPrice) / entryPrice) * 100 } else { return ((entryPrice - currentPrice) / entryPrice) * 100 } } /** * Helper to calculate expected target price */ export function calculateTargetPrice( entryPrice: number, percentChange: number, direction: 'long' | 'short' ): number { if (direction === 'long') { return entryPrice * (1 + percentChange / 100) } else { return entryPrice * (1 - percentChange / 100) } }