diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 4c2f815..6221978 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -760,10 +760,10 @@ export class PositionManager { // trade.currentSize may already be 0 if on-chain orders closed the position before // Position Manager detected it, causing zero P&L bug // HOWEVER: If this was a phantom trade (extreme size mismatch), set P&L to 0 - // CRITICAL FIX: Use tp1Hit flag to determine which size to use for P&L calculation - // - If tp1Hit=false: First closure, calculate on full position size (use originalPositionSize) - // - If tp1Hit=true: Runner closure, calculate on tracked remaining size - const sizeForPnL = trade.tp1Hit ? trade.currentSize : trade.originalPositionSize + + // Use full position size for P&L calculation since we don't know if TP1 filled + // The exit reason logic will determine TP1 vs SL based on profit amount + const sizeForPnL = trade.originalPositionSize // Check if this was a phantom trade by looking at the last known on-chain size // If last on-chain size was <50% of expected, this is a phantom @@ -772,20 +772,15 @@ export class PositionManager { console.log(`📊 External closure detected - Position size tracking:`) console.log(` Original size: $${trade.positionSize.toFixed(2)}`) console.log(` Tracked current size: $${trade.currentSize.toFixed(2)}`) - console.log(` TP1 hit: ${trade.tp1Hit}`) - console.log(` Using for P&L calc: $${sizeForPnL.toFixed(2)} (${trade.tp1Hit ? 'runner' : 'full position'})`) + console.log(` Using for P&L calc: $${sizeForPnL.toFixed(2)} (full position - exit reason will determine TP1 vs SL)`) if (wasPhantom) { console.log(` ⚠️ PHANTOM TRADE: Setting P&L to 0 (size mismatch >50%)`) } - // Determine exit reason based on TP flags and realized P&L - // CRITICAL: Use trade state flags, not current price (on-chain orders filled in the past!) - let exitReason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' = 'SL' - // CRITICAL: Calculate P&L for THIS close only, do NOT add previouslyRealized // Previous bug: Added trade.realizedPnL which could be from prior detection cycles // This caused 10x P&L inflation when same trade detected multiple times - // FIX: Calculate ONLY the runner P&L for this specific closure + // FIX: Calculate ONLY the P&L for this specific closure let runnerRealized = 0 let runnerProfitPercent = 0 if (!wasPhantom) { @@ -797,33 +792,25 @@ export class PositionManager { runnerRealized = (sizeForPnL * runnerProfitPercent) / 100 } - // For external closures, we DON'T know if TP1 already hit, so calculate full position P&L - // Database will have correct previouslyRealized if TP1 was hit - // For external closures, we DON'T know if TP1 already hit, so calculate full position P&L - // Database will have correct previouslyRealized if TP1 was hit const totalRealizedPnL = runnerRealized console.log(` External closure P&L → ${runnerProfitPercent.toFixed(2)}% on $${sizeForPnL.toFixed(2)} = $${totalRealizedPnL.toFixed(2)}`) - - // Determine exit reason from trade state and P&L - if (trade.tp2Hit) { - // TP2 was hit, full position closed (runner stopped or hit target) - exitReason = 'TP2' - } else if (trade.tp1Hit) { - // TP1 was hit, position should be 25% size, but now fully closed - // This means either TP2 filled or runner got stopped out - exitReason = totalRealizedPnL > 0 ? 'TP2' : 'SL' - } else { - // No TPs hit yet - either SL or TP1 filled just now - // Use P&L to determine: positive = TP, negative = SL - if (totalRealizedPnL > trade.positionSize * 0.005) { - // More than 0.5% profit - must be TP1 + // Determine exit reason from P&L percentage + // Use actual profit percent to determine what order filled + let exitReason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' = 'SL' + + if (runnerProfitPercent > 0.3) { + // Positive profit - was a TP order + if (runnerProfitPercent >= 1.2) { + // Large profit (>1.2%) - TP2 range + exitReason = 'TP2' + } else { + // Moderate profit (0.3-1.2%) - TP1 range exitReason = 'TP1' - } else if (totalRealizedPnL < 0) { - // Loss - must be SL - exitReason = 'SL' } - // else: small profit/loss near breakeven, default to SL (could be manual close) + } else { + // Negative or tiny profit - was SL + exitReason = 'SL' } // Update database - CRITICAL: Only update once per trade!