diff --git a/app/api/admin/validate-db/route.ts b/app/api/admin/validate-db/route.ts new file mode 100644 index 0000000..54a3d18 --- /dev/null +++ b/app/api/admin/validate-db/route.ts @@ -0,0 +1,48 @@ +/** + * Manual Database Validation Endpoint + * + * GET /api/admin/validate-db + * + * Triggers immediate validation of database vs Drift positions + * Useful for debugging or manual checks + */ + +import { NextRequest, NextResponse } from 'next/server' +import { runManualValidation } from '@/lib/database/sync-validator' + +export async function GET(request: NextRequest) { + try { + // Optional: Add auth check + const authHeader = request.headers.get('authorization') + const expectedAuth = `Bearer ${process.env.API_SECRET_KEY}` + + if (authHeader && authHeader !== expectedAuth) { + return NextResponse.json( + { success: false, error: 'Unauthorized' }, + { status: 401 } + ) + } + + console.log('🔧 Manual database validation triggered via API') + + const result = await runManualValidation() + + return NextResponse.json({ + success: true, + result, + message: result.ghosts > 0 || result.orphans > 0 + ? `Fixed ${result.ghosts} ghost(s) and ${result.orphans} orphan(s)` + : 'All trades validated successfully' + }) + + } catch (error) { + console.error('❌ Manual validation failed:', error) + return NextResponse.json( + { + success: false, + error: error instanceof Error ? error.message : 'Validation failed' + }, + { status: 500 } + ) + } +} diff --git a/instrumentation.ts b/instrumentation.ts index 816b5d5..afc3261 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -13,6 +13,10 @@ export async function register() { const { initializePositionManagerOnStartup } = await import('./lib/startup/init-position-manager') await initializePositionManagerOnStartup() + // Start database sync validator (runs every 10 minutes) + const { startDatabaseSyncValidator } = await import('./lib/database/sync-validator') + startDatabaseSyncValidator() + console.log('✅ Server initialization complete') } } diff --git a/lib/database/sync-validator.ts b/lib/database/sync-validator.ts new file mode 100644 index 0000000..9847754 --- /dev/null +++ b/lib/database/sync-validator.ts @@ -0,0 +1,220 @@ +/** + * Database-Drift Synchronization Validator + * + * Periodically validates that database "open" trades match actual Drift positions + * Runs independently of Position Manager to catch ghost positions + * + * Ghost positions occur when: + * - On-chain orders fill but database update fails + * - Position Manager closes position but DB write fails + * - Container restarts before cleanup completes + * + * Created: November 16, 2025 + */ + +import { getPrismaClient } from './trades' +import { initializeDriftService } from '../drift/client' +import { getMarketConfig } from '../../config/trading' + +let validationInterval: NodeJS.Timeout | null = null +let isRunning = false + +interface ValidationResult { + checked: number + ghosts: number + orphans: number + valid: number + errors: string[] +} + +/** + * Start periodic validation (runs every 10 minutes) + */ +export function startDatabaseSyncValidator(): void { + if (validationInterval) { + console.log('⚠️ Database sync validator already running') + return + } + + // Run immediately on start + setTimeout(() => validateAllOpenTrades(), 5000) // 5s delay to let system initialize + + // Then run every 10 minutes + validationInterval = setInterval(async () => { + await validateAllOpenTrades() + }, 10 * 60 * 1000) + + console.log('🔍 Database sync validator started (runs every 10 minutes)') +} + +/** + * Stop periodic validation + */ +export function stopDatabaseSyncValidator(): void { + if (validationInterval) { + clearInterval(validationInterval) + validationInterval = null + console.log('🛑 Database sync validator stopped') + } +} + +/** + * Validate all "open" trades in database against Drift positions + * + * This is the master validation that ensures database accuracy + */ +export async function validateAllOpenTrades(): Promise { + if (isRunning) { + console.log('⏭️ Validation already in progress, skipping...') + return { checked: 0, ghosts: 0, orphans: 0, valid: 0, errors: [] } + } + + isRunning = true + const result: ValidationResult = { + checked: 0, + ghosts: 0, + orphans: 0, + valid: 0, + errors: [] + } + + try { + const prisma = getPrismaClient() + + // Get all trades marked as "open" in database + const openTrades = await prisma.trade.findMany({ + where: { exitReason: null }, + orderBy: { createdAt: 'desc' } + }) + + if (openTrades.length === 0) { + console.log('✅ No open trades to validate') + isRunning = false + return result + } + + console.log(`🔍 Validating ${openTrades.length} open trades against Drift...`) + result.checked = openTrades.length + + // Initialize Drift service + let driftService + try { + driftService = await initializeDriftService() + } catch (error) { + const errorMsg = `Failed to initialize Drift service: ${error}` + console.error(`❌ ${errorMsg}`) + result.errors.push(errorMsg) + isRunning = false + return result + } + + // Get all Drift positions (one API call) + let driftPositions + try { + driftPositions = await driftService.getAllPositions() + console.log(`📊 Found ${driftPositions.length} positions on Drift`) + } catch (error) { + const errorMsg = `Failed to fetch Drift positions: ${error}` + console.error(`❌ ${errorMsg}`) + result.errors.push(errorMsg) + isRunning = false + return result + } + + // Validate each database trade + for (const trade of openTrades) { + try { + await validateSingleTrade(trade, driftPositions, result) + } catch (error) { + const errorMsg = `Error validating ${trade.symbol}: ${error}` + console.error(`❌ ${errorMsg}`) + result.errors.push(errorMsg) + } + } + + // Log summary + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log('📊 DATABASE SYNC VALIDATION COMPLETE') + console.log(` Checked: ${result.checked} trades`) + console.log(` ✅ Valid: ${result.valid} (DB matches Drift)`) + console.log(` 👻 Ghosts: ${result.ghosts} (DB open, Drift closed) - FIXED`) + console.log(` 🔄 Orphans: ${result.orphans} (DB closed, Drift open) - FIXED`) + if (result.errors.length > 0) { + console.log(` ⚠️ Errors: ${result.errors.length}`) + } + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + + } catch (error) { + console.error('❌ Database sync validation failed:', error) + result.errors.push(`Validation failed: ${error}`) + } finally { + isRunning = false + } + + return result +} + +/** + * Validate a single trade against Drift positions + */ +async function validateSingleTrade( + trade: any, + driftPositions: any[], + result: ValidationResult +): Promise { + const prisma = getPrismaClient() + + // Find matching Drift position + const driftPosition = driftPositions.find(p => p.symbol === trade.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'}`) + + // 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 + + // 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++ + return + } + + // Position exists on Drift - validate it matches + 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`) + return + } + + // Valid: DB and Drift both show position open with matching direction + result.valid++ +} + +/** + * One-time manual validation (for API endpoint or debugging) + */ +export async function runManualValidation(): Promise { + console.log('🔧 Running manual database validation...') + return await validateAllOpenTrades() +}