fix: TP1/TP2 race condition causing multiple simultaneous closures

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.
This commit is contained in:
mindesbunister
2025-11-14 06:26:59 +01:00
parent 9673457326
commit 31bc08bed4

View File

@@ -699,10 +699,12 @@ export class PositionManager {
// 3. Take profit 1 (closes configured %) // 3. Take profit 1 (closes configured %)
if (!trade.tp1Hit && this.shouldTakeProfit1(currentPrice, trade)) { if (!trade.tp1Hit && this.shouldTakeProfit1(currentPrice, trade)) {
console.log(`🎉 TP1 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) 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 trade.tp1Hit = true
await this.executeExit(trade, this.config.takeProfit1SizePercent, 'TP1', currentPrice)
trade.currentSize = trade.positionSize * ((100 - this.config.takeProfit1SizePercent) / 100) trade.currentSize = trade.positionSize * ((100 - this.config.takeProfit1SizePercent) / 100)
const newStopLossPrice = this.calculatePrice( const newStopLossPrice = this.calculatePrice(
trade.entryPrice, trade.entryPrice,
@@ -782,13 +784,15 @@ export class PositionManager {
if (trade.tp1Hit && !trade.tp2Hit && this.shouldTakeProfit2(currentPrice, trade)) { if (trade.tp1Hit && !trade.tp2Hit && this.shouldTakeProfit2(currentPrice, trade)) {
console.log(`🎊 TP2 HIT: ${trade.symbol} at ${profitPercent.toFixed(2)}%`) 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 // Calculate how much to close based on TP2 size percent
const percentToClose = this.config.takeProfit2SizePercent const percentToClose = this.config.takeProfit2SizePercent
// CRITICAL FIX: If percentToClose is 0, don't call executeExit (would close 100% due to minOrderSize) // 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 // Instead, just mark TP2 as hit and activate trailing stop on full remaining position
if (percentToClose === 0) { if (percentToClose === 0) {
trade.tp2Hit = true
trade.trailingStopActive = true // Activate trailing stop immediately trade.trailingStopActive = true // Activate trailing stop immediately
console.log(`🏃 TP2-as-Runner activated: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% remaining with trailing stop`) 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 // If percentToClose > 0, execute partial close
await this.executeExit(trade, percentToClose, 'TP2', currentPrice) 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) { if (percentToClose < 100) {
trade.tp2Hit = true
trade.currentSize = trade.currentSize * ((100 - percentToClose) / 100) trade.currentSize = trade.currentSize * ((100 - percentToClose) / 100)
console.log(`🏃 Runner activated: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% remaining with trailing stop`) console.log(`🏃 Runner activated: ${((trade.currentSize / trade.positionSize) * 100).toFixed(1)}% remaining with trailing stop`)