/** * 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 let 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 trackedTrades = Array.from(positionManager.getActiveTrades().values()) 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 (Drift size is tokens) const positionSizeUSD = Math.abs(driftPos.size) * currentPrice // Try to find an existing open trade in the database for this symbol const existingTrade = await prisma.trade.findFirst({ where: { symbol: driftPos.symbol, status: 'open', }, orderBy: { entryTime: 'desc' }, }) const normalizeDirection = (dir: string): 'long' | 'short' => dir === 'long' ? 'long' : 'short' const buildActiveTradeFromDb = (dbTrade: any): any => { const pmState = (dbTrade.configSnapshot as any)?.positionManagerState return { id: dbTrade.id, positionId: dbTrade.positionId, symbol: dbTrade.symbol, direction: normalizeDirection(dbTrade.direction), entryPrice: dbTrade.entryPrice, entryTime: dbTrade.entryTime.getTime(), positionSize: dbTrade.positionSizeUSD, leverage: dbTrade.leverage, stopLossPrice: pmState?.stopLossPrice ?? dbTrade.stopLossPrice, tp1Price: dbTrade.takeProfit1Price, tp2Price: dbTrade.takeProfit2Price, emergencyStopPrice: dbTrade.stopLossPrice * (dbTrade.direction === 'long' ? 0.98 : 1.02), currentSize: pmState?.currentSize ?? dbTrade.positionSizeUSD, originalPositionSize: dbTrade.positionSizeUSD, takeProfitPrice1: dbTrade.takeProfit1Price, takeProfitPrice2: dbTrade.takeProfit2Price, tp1Hit: pmState?.tp1Hit ?? false, tp2Hit: pmState?.tp2Hit ?? false, slMovedToBreakeven: pmState?.slMovedToBreakeven ?? false, slMovedToProfit: pmState?.slMovedToProfit ?? false, trailingStopActive: pmState?.trailingStopActive ?? false, realizedPnL: pmState?.realizedPnL ?? 0, unrealizedPnL: pmState?.unrealizedPnL ?? 0, peakPnL: pmState?.peakPnL ?? 0, peakPrice: pmState?.peakPrice ?? dbTrade.entryPrice, maxFavorableExcursion: pmState?.maxFavorableExcursion ?? 0, maxAdverseExcursion: pmState?.maxAdverseExcursion ?? 0, maxFavorablePrice: pmState?.maxFavorablePrice ?? dbTrade.entryPrice, maxAdversePrice: pmState?.maxAdversePrice ?? dbTrade.entryPrice, originalAdx: dbTrade.adxAtEntry, timesScaled: pmState?.timesScaled ?? 0, totalScaleAdded: pmState?.totalScaleAdded ?? 0, atrAtEntry: dbTrade.atrAtEntry, runnerTrailingPercent: pmState?.runnerTrailingPercent, priceCheckCount: 0, lastPrice: pmState?.lastPrice ?? dbTrade.entryPrice, lastUpdateTime: Date.now(), } } let activeTrade if (existingTrade) { console.log(`🔗 Found existing open trade in DB for ${driftPos.symbol}, attaching to Position Manager`) activeTrade = buildActiveTradeFromDb(existingTrade) } else { console.warn(`⚠️ No open DB trade found for ${driftPos.symbol}. Creating synced placeholder to restore protection.`) const now = new Date() const syntheticPositionId = `sync-${now.getTime()}-${driftPos.marketIndex}` const placeholderTrade = await prisma.trade.create({ data: { positionId: syntheticPositionId, symbol: driftPos.symbol, direction, entryPrice, entryTime: now, positionSizeUSD, collateralUSD: positionSizeUSD / config.leverage, leverage: config.leverage, stopLossPrice, takeProfit1Price: tp1Price, takeProfit2Price: tp2Price, tp1SizePercent: config.takeProfit1SizePercent, tp2SizePercent: config.useTp2AsTriggerOnly && (config.takeProfit2SizePercent ?? 0) <= 0 ? 0 : (config.takeProfit2SizePercent ?? 0), status: 'open', signalSource: 'drift_sync', timeframe: 'sync', configSnapshot: { source: 'sync-positions', syncedAt: now.toISOString(), positionManagerState: { currentSize: positionSizeUSD, tp1Hit: false, slMovedToBreakeven: false, slMovedToProfit: false, stopLossPrice, realizedPnL: 0, unrealizedPnL: driftPos.unrealizedPnL ?? 0, peakPnL: driftPos.unrealizedPnL ?? 0, lastPrice: currentPrice, maxFavorableExcursion: 0, maxAdverseExcursion: 0, maxFavorablePrice: entryPrice, maxAdversePrice: entryPrice, lastUpdate: now.toISOString(), }, }, entryOrderTx: syntheticPositionId, }, }) const verifiedPlaceholder = await prisma.trade.findUnique({ where: { positionId: syntheticPositionId } }) if (!verifiedPlaceholder) { throw new Error(`Placeholder trade not persisted for ${driftPos.symbol} (positionId=${syntheticPositionId})`) } activeTrade = buildActiveTradeFromDb(verifiedPlaceholder) } 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 } ) } }