fix: Add phantom trade detection and prevention safeguards
**Root Causes:** 1. Auto-flip logic could create phantom trades if close failed 2. Position size mismatches (0.01 SOL vs 11.92 SOL expected) not caught 3. Multiple trades for same symbol+direction in database **Preventive Measures:** 1. **Startup Validation (lib/startup/init-position-manager.ts)** - Validates all open trades against Drift positions on startup - Auto-closes phantom trades with <50% expected size - Logs size mismatches for manual review - Prevents Position Manager from tracking ghost positions 2. **Duplicate Position Prevention (app/api/trading/execute/route.ts)** - Blocks opening same-direction position on same symbol - Returns 400 error if duplicate detected - Only allows auto-flip (opposite direction close + open) 3. **Runtime Phantom Detection (lib/trading/position-manager.ts)** - Checks position size every 2s monitoring cycle - Auto-closes if size ratio <50% (extreme mismatch) - Logs as 'manual' exit with AUTO_CLEANUP tx - Removes from monitoring immediately 4. **Quality Score Fix (app/api/trading/check-risk/route.ts)** - Hardcoded minScore=60 (removed non-existent config reference) **Prevention Summary:** - ✅ Startup validation catches historical phantoms - ✅ Duplicate check prevents new phantoms - ✅ Runtime detection catches size mismatches <30s after they occur - ✅ All three layers work together for defense-in-depth Issue: User had LONG (phantom) + SHORT (undersized 0.01 SOL vs 11.92 expected) Fix: Both detected and closed, bot now clean with 0 active trades
This commit is contained in:
@@ -489,6 +489,40 @@ export class PositionManager {
|
||||
// Position exists but size mismatch (partial close by TP1?)
|
||||
if (position.size < trade.currentSize * 0.95) { // 5% tolerance
|
||||
console.log(`⚠️ Position size mismatch: expected ${trade.currentSize}, got ${position.size}`)
|
||||
|
||||
// CRITICAL: If mismatch is extreme (>50%), this is a phantom trade
|
||||
const sizeRatio = (position.size * currentPrice) / trade.currentSize
|
||||
if (sizeRatio < 0.5) {
|
||||
console.log(`🚨 EXTREME SIZE MISMATCH (${(sizeRatio * 100).toFixed(1)}%) - Closing phantom trade`)
|
||||
console.log(` Expected: $${trade.currentSize.toFixed(2)}`)
|
||||
console.log(` Actual: $${(position.size * currentPrice).toFixed(2)}`)
|
||||
|
||||
// Close as phantom trade
|
||||
try {
|
||||
const holdTimeSeconds = Math.floor((Date.now() - trade.entryTime) / 1000)
|
||||
await updateTradeExit({
|
||||
positionId: trade.positionId,
|
||||
exitPrice: currentPrice,
|
||||
exitReason: 'manual',
|
||||
realizedPnL: 0,
|
||||
exitOrderTx: 'AUTO_CLEANUP',
|
||||
holdTimeSeconds,
|
||||
maxDrawdown: Math.abs(Math.min(0, trade.maxAdverseExcursion)),
|
||||
maxGain: Math.max(0, trade.maxFavorableExcursion),
|
||||
maxFavorableExcursion: trade.maxFavorableExcursion,
|
||||
maxAdverseExcursion: trade.maxAdverseExcursion,
|
||||
maxFavorablePrice: trade.maxFavorablePrice,
|
||||
maxAdversePrice: trade.maxAdversePrice,
|
||||
})
|
||||
console.log(`💾 Phantom trade closed`)
|
||||
} catch (dbError) {
|
||||
console.error('❌ Failed to close phantom trade:', dbError)
|
||||
}
|
||||
|
||||
await this.removeTrade(trade.id)
|
||||
return
|
||||
}
|
||||
|
||||
// Update current size to match reality (convert base asset size to USD using current price)
|
||||
trade.currentSize = position.size * currentPrice
|
||||
trade.tp1Hit = true
|
||||
|
||||
Reference in New Issue
Block a user