From 0ea8773bdc67be38d3b9c17a71a0674601b5d93d Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Mon, 3 Nov 2025 00:02:19 +0100 Subject: [PATCH] fix: detect exit reason using trade state flags instead of current price CRITICAL BUG: Position Manager was using current price to determine exit reason, but on-chain orders filled at a DIFFERENT price in the past! Example: LONG entry $184.55, TP1 filled at $184.66, but when Position Manager checked later (price dropped), it saw currentPrice < TP1 and defaulted to 'SL' Result: Profitable trades incorrectly labeled as SL exits in database Fix: - Use trade.tp1Hit and trade.tp2Hit flags to determine exit reason - If no TP flags set, use realized P&L to distinguish: - Profit >0.5% = TP1 filled - Negative P&L = SL filled - Remove duplicate P&L calculation This ensures exit reasons match actual on-chain order fills --- lib/trading/position-manager.ts | 45 ++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index a3d7f3b..c88596d 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -407,29 +407,11 @@ export class PositionManager { // Save currentSize before it becomes 0 const sizeBeforeClosure = trade.currentSize - // Determine exit reason based on price + // 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' - if (trade.direction === 'long') { - if (currentPrice >= trade.tp2Price) { - exitReason = 'TP2' - } else if (currentPrice >= trade.tp1Price) { - exitReason = 'TP1' - } else if (currentPrice <= trade.stopLossPrice) { - exitReason = 'HARD_SL' // Assume hard stop if below SL - } - } else { - // Short - if (currentPrice <= trade.tp2Price) { - exitReason = 'TP2' - } else if (currentPrice <= trade.tp1Price) { - exitReason = 'TP1' - } else if (currentPrice >= trade.stopLossPrice) { - exitReason = 'HARD_SL' // Assume hard stop if above SL - } - } - - // Calculate final P&L using size BEFORE closure + // Calculate P&L first const profitPercent = this.calculateProfitPercent( trade.entryPrice, currentPrice, @@ -438,6 +420,27 @@ export class PositionManager { const accountPnL = profitPercent * trade.leverage const realizedPnL = (sizeBeforeClosure * accountPnL) / 100 + // 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 = realizedPnL > 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 (realizedPnL > trade.positionSize * 0.005) { + // More than 0.5% profit - must be TP1 + exitReason = 'TP1' + } else if (realizedPnL < 0) { + // Loss - must be SL + exitReason = 'SL' + } + // else: small profit/loss near breakeven, default to SL (could be manual close) + } + // Update database const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000) try {