From 466c0c80018b9fda6d6b74b4ecb689dbb18a1327 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Sat, 1 Nov 2025 20:06:14 +0100 Subject: [PATCH] fix: runner tracking bug - detect TP fills by size reduction - Position Manager now detects TP1/TP2 fills by monitoring position size reductions instead of entry price mismatches - When position size reduces by ~75%, marks TP1 as filled and updates currentSize - When position size reduces by ~95%, marks TP2 as filled and activates trailing stop for 5% runner - Entry price mismatch check now skipped after TP fills (Drift shows weighted average entry price after partial closes) - Fixes bug where runners were incorrectly closed after TP1/TP2 fired on-chain - Adds grace period for new trades (<30s) to avoid false positives during blockchain propagation delays - This unblocks Phase 1 data collection for signal quality optimization (need 10+ trades with MAE/MFE data) --- lib/trading/position-manager.ts | 113 +++++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 32 deletions(-) diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index 5ca5ec3..51977f8 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -297,46 +297,95 @@ export class PositionManager { return // Skip this check cycle, position might still be propagating } - // Position closed externally (by on-chain TP/SL order) + // Position closed externally (by on-chain TP/SL order or manual closure) console.log(`⚠️ Position ${trade.symbol} was closed externally (by on-chain order)`) } else { - // CRITICAL: Verify this position matches the trade we're tracking - // Different entry price means this is a NEW position, not ours - const entryPriceDiff = Math.abs(position.entryPrice - trade.entryPrice) - const entryPriceDiffPercent = (entryPriceDiff / trade.entryPrice) * 100 + // Position exists - check if size changed (TP1/TP2 filled) + const positionSizeUSD = position.size * currentPrice + const trackedSizeUSD = trade.currentSize + const sizeDiffPercent = Math.abs(positionSizeUSD - trackedSizeUSD) / trackedSizeUSD * 100 - if (entryPriceDiffPercent > 0.5) { - // Entry prices differ by >0.5% - this is a DIFFERENT position - console.log(`⚠️ Position ${trade.symbol} entry mismatch: tracking $${trade.entryPrice.toFixed(4)} but found $${position.entryPrice.toFixed(4)}`) - console.log(`🗑️ This is a different/newer position - removing old trade from monitoring`) + // If position size reduced significantly, TP orders likely filled + if (positionSizeUSD < trackedSizeUSD * 0.9 && sizeDiffPercent > 10) { + console.log(`📊 Position size changed: tracking $${trackedSizeUSD.toFixed(2)} but found $${positionSizeUSD.toFixed(2)}`) - // Mark the old trade as closed (we lost track of it) - try { - await updateTradeExit({ - positionId: trade.positionId, - exitPrice: trade.lastPrice, - exitReason: 'SOFT_SL', // Unknown - just mark as closed - realizedPnL: 0, - exitOrderTx: 'UNKNOWN_CLOSURE', - holdTimeSeconds: Math.floor((Date.now() - trade.entryTime) / 1000), - maxDrawdown: 0, - maxGain: trade.peakPnL, - }) - console.log(`💾 Old trade marked as closed (lost tracking)`) - } catch (dbError) { - console.error('❌ Failed to save lost trade closure:', dbError) - } - - // Remove from monitoring WITHOUT cancelling orders (they belong to the new position!) - console.log(`🗑️ Removing old trade WITHOUT cancelling orders`) - this.activeTrades.delete(trade.id) - - if (this.activeTrades.size === 0 && this.isMonitoring) { - this.stopMonitoring() + // Detect which TP filled based on size reduction + const reductionPercent = ((trackedSizeUSD - positionSizeUSD) / trade.positionSize) * 100 + + if (!trade.tp1Hit && reductionPercent >= (this.config.takeProfit1SizePercent * 0.8)) { + // TP1 fired (should be ~75% reduction) + console.log(`🎯 TP1 detected as filled! Reduction: ${reductionPercent.toFixed(1)}%`) + trade.tp1Hit = true + trade.currentSize = positionSizeUSD + + // Move SL to breakeven after TP1 + trade.stopLossPrice = trade.entryPrice + trade.slMovedToBreakeven = true + console.log(`🛡️ Stop loss moved to breakeven: $${trade.stopLossPrice.toFixed(4)}`) + + await this.saveTradeState(trade) + + } else if (trade.tp1Hit && !trade.tp2Hit && reductionPercent >= 85) { + // TP2 fired (total should be ~95% closed, 5% runner left) + console.log(`🎯 TP2 detected as filled! Reduction: ${reductionPercent.toFixed(1)}%`) + trade.tp2Hit = true + trade.currentSize = positionSizeUSD + trade.trailingStopActive = true + console.log(`🏃 Runner active: $${positionSizeUSD.toFixed(2)} with ${this.config.trailingStopPercent}% trailing stop`) + + await this.saveTradeState(trade) + + } else { + // Partial fill detected but unclear which TP - just update size + console.log(`⚠️ Unknown partial fill detected - updating tracked size to $${positionSizeUSD.toFixed(2)}`) + trade.currentSize = positionSizeUSD + await this.saveTradeState(trade) } + // Continue monitoring the remaining position return } + + // CRITICAL: Check for entry price mismatch (NEW position opened) + // This can happen if user manually closed and opened a new position + // Only check if we haven't detected TP fills (entry price changes after partial closes on Drift) + if (!trade.tp1Hit && !trade.tp2Hit) { + const entryPriceDiff = Math.abs(position.entryPrice - trade.entryPrice) + const entryPriceDiffPercent = (entryPriceDiff / trade.entryPrice) * 100 + + if (entryPriceDiffPercent > 0.5) { + // Entry prices differ by >0.5% - this is a DIFFERENT position + console.log(`⚠️ Position ${trade.symbol} entry mismatch: tracking $${trade.entryPrice.toFixed(4)} but found $${position.entryPrice.toFixed(4)}`) + console.log(`🗑️ This is a different/newer position - removing old trade from monitoring`) + + // Mark the old trade as closed (we lost track of it) + try { + await updateTradeExit({ + positionId: trade.positionId, + exitPrice: trade.lastPrice, + exitReason: 'SOFT_SL', // Unknown - just mark as closed + realizedPnL: 0, + exitOrderTx: 'UNKNOWN_CLOSURE', + holdTimeSeconds: Math.floor((Date.now() - trade.entryTime) / 1000), + maxDrawdown: 0, + maxGain: trade.peakPnL, + }) + console.log(`💾 Old trade marked as closed (lost tracking)`) + } catch (dbError) { + console.error('❌ Failed to save lost trade closure:', dbError) + } + + // Remove from monitoring WITHOUT cancelling orders (they belong to the new position!) + console.log(`🗑️ Removing old trade WITHOUT cancelling orders`) + this.activeTrades.delete(trade.id) + + if (this.activeTrades.size === 0 && this.isMonitoring) { + this.stopMonitoring() + } + + return + } + } } if (position === null || position.size === 0) {