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)
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user