/** * Position Manager Startup Initialization * * Ensures Position Manager starts monitoring on bot startup * This prevents orphaned trades when the bot restarts */ import { getInitializedPositionManager } from '../trading/position-manager' import { initializeDriftService } from '../drift/client' import { getPrismaClient } from '../database/trades' import { getMarketConfig } from '../../config/trading' let initStarted = false export async function initializePositionManagerOnStartup() { if (initStarted) { return } initStarted = true console.log('🚀 Initializing Position Manager on startup...') try { // CRITICAL: Run database sync validator FIRST to clean up duplicates const { validateAllOpenTrades } = await import('../database/sync-validator') console.log('🔍 Running database sync validation before Position Manager init...') const validationResult = await validateAllOpenTrades() if (validationResult.ghosts > 0) { console.log(`✅ Cleaned up ${validationResult.ghosts} ghost/duplicate trades`) } // Then validate open trades against Drift positions await validateOpenTrades() const manager = await getInitializedPositionManager() const status = manager.getStatus() console.log(`✅ Position Manager ready - ${status.activeTradesCount} active trades`) if (status.activeTradesCount > 0) { console.log(`📊 Monitoring: ${status.symbols.join(', ')}`) } } catch (error) { console.error('❌ Failed to initialize Position Manager on startup:', error) } } /** * Validate that open trades in database match actual Drift positions * * CRITICAL FIX (Nov 14, 2025): * - Also checks trades marked as "closed" in DB that might still be open on Drift * - Happens when close transaction fails but bot marks it as closed anyway * - Restores Position Manager tracking for these orphaned positions */ async function validateOpenTrades() { try { const prisma = getPrismaClient() // Get both truly open trades AND recently "closed" trades (last 24h) // Recently closed trades might still be open if close transaction failed // TEMPORARILY REDUCED: Check only last 5 closed trades to avoid rate limiting on startup const [openTrades, recentlyClosedTrades] = await Promise.all([ prisma.trade.findMany({ where: { status: 'open' }, orderBy: { entryTime: 'asc' } }), prisma.trade.findMany({ where: { exitReason: { not: null }, exitTime: { gte: new Date(Date.now() - 6 * 60 * 60 * 1000) } // Last 6 hours (reduced from 24h) }, orderBy: { exitTime: 'desc' }, take: 5 // Reduced from 20 to avoid rate limiting }) ]) const allTradesToCheck = [...openTrades, ...recentlyClosedTrades] if (allTradesToCheck.length === 0) { console.log('✅ No open trades to validate') return } console.log(`🔍 Validating ${openTrades.length} open + ${recentlyClosedTrades.length} recently closed trades against Drift...`) // CRITICAL: Group trades by symbol to handle multiple DB entries for same Drift position // This prevents reopening old closed trades when only the most recent should be restored const tradesBySymbol = new Map() for (const trade of allTradesToCheck) { const existing = tradesBySymbol.get(trade.symbol) || [] existing.push(trade) tradesBySymbol.set(trade.symbol, existing) } const driftService = await initializeDriftService() const driftPositions = await driftService.getAllPositions() // Get all positions once // Process each symbol's trades (keep only most recent if multiple exist) for (const [symbol, trades] of tradesBySymbol) { // Sort by creation time, newest first trades.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) const mostRecentTrade = trades[0] const olderTrades = trades.slice(1) // Close any older trades BEFORE validating the most recent for (const oldTrade of olderTrades) { if (oldTrade.exitReason === null) { console.log(`🗑️ Closing duplicate old trade: ${oldTrade.id} (${symbol}, created ${oldTrade.createdAt.toISOString()})`) await prisma.trade.update({ where: { id: oldTrade.id }, data: { status: 'closed', exitTime: new Date(), exitReason: 'DUPLICATE_CLEANUP', exitPrice: oldTrade.entryPrice, realizedPnL: 0, } }) } } // Now validate only the most recent trade for this symbol const trade = mostRecentTrade try { const marketConfig = getMarketConfig(trade.symbol) // Find matching Drift position by symbol const position = driftPositions.find(p => p.symbol === trade.symbol) if (!position || position.size === 0) { // No position on Drift if (trade.status === 'open') { console.log(`⚠️ PHANTOM TRADE: ${trade.symbol} marked open in DB but not found on Drift`) console.log(` 🗑️ Auto-closing phantom trade...`) await prisma.trade.update({ where: { id: trade.id }, data: { status: 'closed', exitTime: new Date(), exitReason: 'PHANTOM_TRADE_CLEANUP', exitPrice: trade.entryPrice, realizedPnL: 0, realizedPnLPercent: 0, } }) } // If already closed in DB and not on Drift, that's correct - skip continue } // Position EXISTS on Drift const driftDirection = position.side.toLowerCase() as 'long' | 'short' if (driftDirection !== trade.direction) { console.log(`⚠️ DIRECTION MISMATCH: ${trade.symbol} DB=${trade.direction} Drift=${driftDirection}`) continue } // CRITICAL: If DB says closed but Drift shows open, restore tracking! if (trade.exitReason !== null) { console.log(`🔴 CRITICAL: ${trade.symbol} marked as CLOSED in DB but still OPEN on Drift!`) console.log(` DB entry: $${trade.entryPrice.toFixed(2)} | Drift entry: $${position.entryPrice.toFixed(2)}`) console.log(` DB exit: ${trade.exitReason} at ${trade.exitTime?.toISOString()}`) console.log(` Drift: ${position.size} ${trade.symbol} ${driftDirection} @ $${position.entryPrice.toFixed(2)}`) console.log(` 🔄 Reopening trade and correcting entry price to match Drift...`) // Calculate position size in USD using Drift's entry price const currentPrice = await driftService.getOraclePrice(marketConfig.driftMarketIndex) const positionSizeUSD = position.size * currentPrice await prisma.trade.update({ where: { id: trade.id }, data: { status: 'open', exitReason: null, exitTime: null, exitPrice: null, entryPrice: position.entryPrice, // CRITICAL: Use Drift's actual entry price, not DB value positionSizeUSD: positionSizeUSD, // Update to current size (may be runner after TP1) // Keep original realizedPnL from partial closes if any } }) console.log(` ✅ Trade restored with corrected entry: $${position.entryPrice.toFixed(2)} (was $${trade.entryPrice.toFixed(2)})`) } else { console.log(`✅ ${trade.symbol} ${trade.direction}: Position verified on Drift`) } } catch (posError) { console.error(`❌ Error validating trade ${trade.symbol}:`, posError) } } } catch (error) { console.error('❌ Error in validateOpenTrades:', error) } }