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:
mindesbunister
2025-11-01 20:06:14 +01:00
parent 056440bf8f
commit 466c0c8001

View File

@@ -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) {