diff --git a/lib/database/sync-validator.ts b/lib/database/sync-validator.ts index 9847754..089f2c5 100644 --- a/lib/database/sync-validator.ts +++ b/lib/database/sync-validator.ts @@ -112,7 +112,7 @@ export async function validateAllOpenTrades(): Promise { let driftPositions try { driftPositions = await driftService.getAllPositions() - console.log(`📊 Found ${driftPositions.length} positions on Drift`) + console.log(`📊 Found ${driftPositions.length} actual positions on Drift`) } catch (error) { const errorMsg = `Failed to fetch Drift positions: ${error}` console.error(`❌ ${errorMsg}`) @@ -121,12 +121,20 @@ export async function validateAllOpenTrades(): Promise { return result } - // Validate each database trade + // Group database trades by symbol to handle multiple DB entries for same position + const tradesBySymbol = new Map() for (const trade of openTrades) { + const existing = tradesBySymbol.get(trade.symbol) || [] + existing.push(trade) + tradesBySymbol.set(trade.symbol, existing) + } + + // Validate each symbol's trades against Drift position + for (const [symbol, trades] of tradesBySymbol) { try { - await validateSingleTrade(trade, driftPositions, result) + await validateSymbolTrades(symbol, trades, driftPositions, result) } catch (error) { - const errorMsg = `Error validating ${trade.symbol}: ${error}` + const errorMsg = `Error validating ${symbol}: ${error}` console.error(`❌ ${errorMsg}`) result.errors.push(errorMsg) } @@ -155,60 +163,97 @@ export async function validateAllOpenTrades(): Promise { } /** - * Validate a single trade against Drift positions + * Validate all trades for a symbol against the Drift position + * + * CRITICAL: Multiple DB trades can exist for one Drift position + * This happens because old trades get re-opened by startup validator + * Only the MOST RECENT trade should be kept open */ -async function validateSingleTrade( - trade: any, +async function validateSymbolTrades( + symbol: string, + trades: any[], driftPositions: any[], result: ValidationResult ): Promise { const prisma = getPrismaClient() // Find matching Drift position - const driftPosition = driftPositions.find(p => p.symbol === trade.symbol) + const driftPosition = driftPositions.find(p => p.symbol === symbol) if (!driftPosition || Math.abs(driftPosition.size) < 0.01) { - // GHOST DETECTED: Database says open, but Drift says closed/missing - console.log(`👻 GHOST DETECTED: ${trade.symbol} ${trade.direction}`) - console.log(` DB: Open since ${trade.createdAt.toISOString()}`) - console.log(` Drift: Position not found or size = 0`) - console.log(` P&L: ${trade.realizedPnL ? `$${trade.realizedPnL.toFixed(2)}` : 'null'}`) + // NO POSITION ON DRIFT - all DB trades for this symbol are ghosts + console.log(`👻 GHOST DETECTED: ${symbol} - ${trades.length} DB trades but no Drift position`) - // Check if this is a closed position with P&L but missing exitReason - const hasRealizedPnL = trade.realizedPnL !== null && trade.realizedPnL !== undefined - const exitReason = hasRealizedPnL ? 'manual' : 'GHOST_CLEANUP' - const exitPrice = trade.exitPrice || trade.entryPrice - const realizedPnL = trade.realizedPnL || 0 + for (const trade of trades) { + const hasRealizedPnL = trade.realizedPnL !== null && trade.realizedPnL !== undefined + const exitReason = hasRealizedPnL ? 'manual' : 'GHOST_CLEANUP' + const exitPrice = trade.exitPrice || trade.entryPrice + const realizedPnL = trade.realizedPnL || 0 - // Mark as closed in database - await prisma.trade.update({ - where: { id: trade.id }, - data: { - exitReason: exitReason, - exitTime: new Date(), - exitPrice: exitPrice, - realizedPnL: realizedPnL, - status: 'closed' - } - }) - - console.log(` ✅ Marked as closed (reason: ${exitReason})`) - result.ghosts++ + await prisma.trade.update({ + where: { id: trade.id }, + data: { + exitReason: exitReason, + exitTime: new Date(), + exitPrice: exitPrice, + realizedPnL: realizedPnL, + status: 'closed' + } + }) + + console.log(` ✅ Closed ghost trade ${trade.id} (${exitReason})`) + result.ghosts++ + } return } - // Position exists on Drift - validate it matches + // POSITION EXISTS ON DRIFT const driftDirection = driftPosition.side.toLowerCase() - if (driftDirection !== trade.direction) { - console.log(`⚠️ DIRECTION MISMATCH: ${trade.symbol}`) - console.log(` DB: ${trade.direction} | Drift: ${driftDirection}`) - result.errors.push(`${trade.symbol}: Direction mismatch`) + // Check if any trade has wrong direction + const wrongDirection = trades.find(t => t.direction !== driftDirection) + if (wrongDirection) { + console.log(`⚠️ DIRECTION MISMATCH: ${symbol}`) + console.log(` DB: ${wrongDirection.direction} | Drift: ${driftDirection}`) + result.errors.push(`${symbol}: Direction mismatch`) return } - // Valid: DB and Drift both show position open with matching direction - result.valid++ + // If multiple trades exist for same symbol, keep only the MOST RECENT + if (trades.length > 1) { + console.log(`🔄 DUPLICATE TRADES: ${symbol} has ${trades.length} open DB entries for 1 Drift position`) + + // Sort by creation time, keep newest + trades.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()) + const keepTrade = trades[0] + const closeTrades = trades.slice(1) + + console.log(` ✅ Keeping: ${keepTrade.id} (${keepTrade.createdAt.toISOString()})`) + + for (const trade of closeTrades) { + const hasRealizedPnL = trade.realizedPnL !== null && trade.realizedPnL !== undefined + const exitReason = hasRealizedPnL ? 'manual' : 'DUPLICATE_CLEANUP' + + await prisma.trade.update({ + where: { id: trade.id }, + data: { + exitReason: exitReason, + exitTime: new Date(), + exitPrice: trade.exitPrice || trade.entryPrice, + realizedPnL: trade.realizedPnL || 0, + status: 'closed' + } + }) + + console.log(` 🗑️ Closed duplicate: ${trade.id} (${trade.createdAt.toISOString()})`) + result.ghosts++ + } + + result.valid++ // Count the one we kept + } else { + // Single trade matches single Drift position - all good + result.valid++ + } } /**