/** * Sync Positions API Endpoint * * Re-synchronizes Position Manager with actual Drift positions * Useful when: * - Partial fills cause tracking issues * - Bot restarts and loses in-memory state * - Manual interventions on Drift * - Database gets out of sync * * POST /api/trading/sync-positions */ import { NextRequest, NextResponse } from 'next/server' import { initializeDriftService, getDriftService } from '@/lib/drift/client' import { getInitializedPositionManager } from '@/lib/trading/position-manager' import { getPrismaClient } from '@/lib/database/trades' import { getMergedConfig } from '@/config/trading' export async function POST(request: NextRequest): Promise { try { console.log('🔄 Position sync requested...') const config = getMergedConfig() const driftService = await initializeDriftService() const positionManager = await getInitializedPositionManager() const prisma = getPrismaClient() // Get all current Drift positions const driftPositions = await driftService.getAllPositions() console.log(`📊 Found ${driftPositions.length} positions on Drift`) // Get all currently tracked positions const trackedTrades = Array.from(positionManager.getActiveTrades().values()) console.log(`📋 Position Manager tracking ${trackedTrades.length} trades`) const syncResults = { drift_positions: driftPositions.length, tracked_positions: trackedTrades.length, added: [] as string[], removed: [] as string[], unchanged: [] as string[], errors: [] as string[], } // Step 1: Remove tracked positions that don't exist on Drift for (const trade of trackedTrades) { const existsOnDrift = driftPositions.some(p => p.symbol === trade.symbol) if (!existsOnDrift) { console.log(`🗑️ Removing ${trade.symbol} (not on Drift)`) await positionManager.removeTrade(trade.id) syncResults.removed.push(trade.symbol) // Mark as closed in database try { await prisma.trade.update({ where: { positionId: trade.positionId }, data: { status: 'closed', exitReason: 'sync_cleanup', exitTime: new Date(), }, }) } catch (dbError) { console.error(`❌ Failed to update database for ${trade.symbol}:`, dbError) } } else { syncResults.unchanged.push(trade.symbol) } } // Step 2: Add Drift positions that aren't being tracked for (const driftPos of driftPositions) { const isTracked = trackedTrades.some(t => t.symbol === driftPos.symbol) if (!isTracked) { console.log(`➕ Adding ${driftPos.symbol} to Position Manager`) try { // Get current oracle price for this market const currentPrice = await driftService.getOraclePrice(driftPos.marketIndex) // Calculate targets based on current config const direction = driftPos.side const entryPrice = driftPos.entryPrice // Calculate TP/SL prices const calculatePrice = (entry: number, percent: number, dir: 'long' | 'short') => { if (dir === 'long') { return entry * (1 + percent / 100) } else { return entry * (1 - percent / 100) } } const stopLossPrice = calculatePrice(entryPrice, config.stopLossPercent, direction) const tp1Price = calculatePrice(entryPrice, config.takeProfit1Percent, direction) const tp2Price = calculatePrice(entryPrice, config.takeProfit2Percent, direction) const emergencyStopPrice = calculatePrice(entryPrice, config.emergencyStopPercent, direction) // Calculate position size in USD const positionSizeUSD = driftPos.size * currentPrice // Create ActiveTrade object const activeTrade = { id: `sync-${Date.now()}-${driftPos.symbol}`, positionId: `manual-${Date.now()}`, // Synthetic ID since we don't have the original symbol: driftPos.symbol, direction: direction, entryPrice: entryPrice, entryTime: Date.now() - (60 * 60 * 1000), // Assume 1 hour ago (we don't know actual time) positionSize: positionSizeUSD, leverage: config.leverage, stopLossPrice: stopLossPrice, tp1Price: tp1Price, tp2Price: tp2Price, emergencyStopPrice: emergencyStopPrice, currentSize: positionSizeUSD, originalPositionSize: positionSizeUSD, // Store original size for P&L takeProfitPrice1: tp1Price, takeProfitPrice2: tp2Price, tp1Hit: false, tp2Hit: false, slMovedToBreakeven: false, slMovedToProfit: false, trailingStopActive: false, realizedPnL: 0, unrealizedPnL: driftPos.unrealizedPnL, peakPnL: driftPos.unrealizedPnL, peakPrice: currentPrice, maxFavorableExcursion: 0, maxAdverseExcursion: 0, maxFavorablePrice: currentPrice, maxAdversePrice: currentPrice, originalAdx: undefined, timesScaled: 0, totalScaleAdded: 0, atrAtEntry: undefined, runnerTrailingPercent: undefined, priceCheckCount: 0, lastPrice: currentPrice, lastUpdateTime: Date.now(), } await positionManager.addTrade(activeTrade) syncResults.added.push(driftPos.symbol) console.log(`✅ Added ${driftPos.symbol} to monitoring`) } catch (error) { console.error(`❌ Failed to add ${driftPos.symbol}:`, error) syncResults.errors.push(`${driftPos.symbol}: ${error instanceof Error ? error.message : 'Unknown error'}`) } } } const summary = { success: true, message: 'Position sync complete', results: syncResults, details: { drift_positions: driftPositions.map(p => ({ symbol: p.symbol, direction: p.side, size: p.size, entry: p.entryPrice, pnl: p.unrealizedPnL, })), now_tracking: Array.from(positionManager.getActiveTrades().values()).map(t => ({ symbol: t.symbol, direction: t.direction, entry: t.entryPrice, })), }, } console.log('✅ Position sync complete') console.log(` Added: ${syncResults.added.length}`) console.log(` Removed: ${syncResults.removed.length}`) console.log(` Unchanged: ${syncResults.unchanged.length}`) console.log(` Errors: ${syncResults.errors.length}`) return NextResponse.json(summary) } catch (error) { console.error('❌ Position sync error:', error) return NextResponse.json( { success: false, error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error', }, { status: 500 } ) } }