From 31bc08bed436a4aefd86174994988d3af0feef1e Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Fri, 14 Nov 2025 06:26:59 +0100 Subject: [PATCH] fix: TP1/TP2 race condition causing multiple simultaneous closures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL BUG FIX: - Position Manager monitoring loop (every 2s) could trigger TP1/TP2 multiple times - tp1Hit flag was set AFTER async executeExit() completed - Multiple concurrent executeExit() calls happened before flag was set - Result: Position closed 6 times (70% close × 6 = entire position + failed attempts) ROOT CAUSE: - Race window: ~0.5-1s between check and flag set - Multiple monitoring loops entered if statement simultaneously FIX APPLIED: - Set tp1Hit = true IMMEDIATELY before calling executeExit() - Same fix for tp2Hit flag - Prevents concurrent execution by setting flag synchronously EVIDENCE: - Test trade at 04:47:09: TP1 triggered 6 times - First close: Remaining $13.52 (correct 30%) - Closes 2-6: Remaining $0.00 (closed entire position) - Position Manager continued tracking $13.02 runner that didn't exist IMPACT: - User had unprotected $42.73 position (Position Manager tracking phantom) - No TP/SL monitoring, no trailing stop - Had to manually close position Files changed: - lib/trading/position-manager.ts: Move tp1Hit/tp2Hit flag setting before async calls - Prevents race condition on all future trades Testing required: Execute test trade and verify TP1 triggers only once. --- lib/trading/position-manager.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 55d1a18..9475f51 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -699,10 +699,12 @@ export class PositionManager { // 3. Take profit 1 (closes configured %) if (!trade.tp1Hit && this.shouldTakeProfit1(currentPrice, trade)) { console.log(`🎉 TP1 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) - await this.executeExit(trade, this.config.takeProfit1SizePercent, 'TP1', currentPrice) - // Move SL based on breakEvenTriggerPercent setting + // CRITICAL: Set flag BEFORE async executeExit to prevent race condition + // Multiple monitoring loops can trigger TP1 simultaneously if we wait until after trade.tp1Hit = true + + await this.executeExit(trade, this.config.takeProfit1SizePercent, 'TP1', currentPrice) trade.currentSize = trade.positionSize * ((100 - this.config.takeProfit1SizePercent) / 100) const newStopLossPrice = this.calculatePrice( trade.entryPrice, @@ -782,13 +784,15 @@ export class PositionManager { if (trade.tp1Hit && !trade.tp2Hit && this.shouldTakeProfit2(currentPrice, trade)) { console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) + // CRITICAL: Set flag BEFORE any async operations to prevent race condition + trade.tp2Hit = true + // Calculate how much to close based on TP2 size percent const percentToClose = this.config.takeProfit2SizePercent // CRITICAL FIX: If percentToClose is 0, don't call executeExit (would close 100% due to minOrderSize) // Instead, just mark TP2 as hit and activate trailing stop on full remaining position if (percentToClose === 0) { - trade.tp2Hit = true trade.trailingStopActive = true // Activate trailing stop immediately console.log(`🏃 TP2-as-Runner activated: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% remaining with trailing stop`) @@ -803,9 +807,8 @@ export class PositionManager { // If percentToClose > 0, execute partial close await this.executeExit(trade, percentToClose, 'TP2', currentPrice) - // If some position remains, mark TP2 as hit and activate trailing stop + // If some position remains, update size and activate trailing stop if (percentToClose < 100) { - trade.tp2Hit = true trade.currentSize = trade.currentSize * ((100 - percentToClose) / 100) console.log(`🏃 Runner activated: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% remaining with trailing stop`)