/** * Position Sync Helper * * Extracted from sync-positions endpoint to allow internal reuse * by health monitor for automatic position synchronization. * * Created: Dec 12, 2025 * Enhanced: Dec 12, 2025 - Added order signature discovery for synced positions */ import { getDriftService } from '../drift/client' import { getPrismaClient } from '../database/trades' import { getMergedConfig } from '@/config/trading' import { OrderType, OrderTriggerCondition } from '@drift-labs/sdk' /** * Query Drift for existing orders on a position and extract signatures * CRITICAL FIX (Dec 12, 2025): Auto-synced positions need order signatures * so Position Manager can update orders after TP1/TP2 events */ async function discoverExistingOrders(symbol: string, marketIndex: number): Promise<{ tp1OrderTx?: string tp2OrderTx?: string slOrderTx?: string softStopOrderTx?: string hardStopOrderTx?: string }> { try { const driftService = getDriftService() const driftClient = driftService.getClient() // Get all open orders for this user const orders = driftClient.getOpenOrders() // Filter for orders on this market that are reduce-only const marketOrders = orders.filter(order => order.marketIndex === marketIndex && order.reduceOnly === true ) const signatures: any = {} for (const order of marketOrders) { // Try to extract transaction signature from order reference // Note: Drift SDK may not expose TX signatures directly on orders // This is a best-effort attempt to recover order references const orderRefStr = order.orderId?.toString() || '' if (order.orderType === OrderType.LIMIT) { // TP1 or TP2 (LIMIT orders) // Higher trigger = TP for long, lower trigger = TP for short if (!signatures.tp1OrderTx) { signatures.tp1OrderTx = orderRefStr console.log(`🔍 Found potential TP1 order: ${orderRefStr.slice(0, 8)}...`) } else if (!signatures.tp2OrderTx) { signatures.tp2OrderTx = orderRefStr console.log(`🔍 Found potential TP2 order: ${orderRefStr.slice(0, 8)}...`) } } else if (order.orderType === OrderType.TRIGGER_MARKET) { // Hard stop loss (TRIGGER_MARKET) if (!signatures.slOrderTx && !signatures.hardStopOrderTx) { signatures.hardStopOrderTx = orderRefStr console.log(`🔍 Found hard SL order: ${orderRefStr.slice(0, 8)}...`) } } else if (order.orderType === OrderType.TRIGGER_LIMIT) { // Soft stop loss (TRIGGER_LIMIT) if (!signatures.softStopOrderTx) { signatures.softStopOrderTx = orderRefStr console.log(`🔍 Found soft SL order: ${orderRefStr.slice(0, 8)}...`) } } } // Log what we found const foundCount = Object.keys(signatures).filter(k => signatures[k]).length if (foundCount > 0) { console.log(`✅ Discovered ${foundCount} existing order(s) for ${symbol}`) } else { console.warn(`⚠️ No existing orders found for ${symbol} - PM won't be able to update orders after events`) } return signatures } catch (error) { console.error(`❌ Error discovering orders for ${symbol}:`, error) return {} // Return empty object on error (fail gracefully) } } /** * Sync a single Drift position to Position Manager */ export async function syncSinglePosition(driftPos: any, positionManager: any): Promise { const driftService = getDriftService() const prisma = getPrismaClient() const config = getMergedConfig() 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, trailingStopDistance: pmState?.trailingStopDistance, 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, // TP2 runner configuration (CRITICAL FIX - Dec 12, 2025) tp1SizePercent: pmState?.tp1SizePercent ?? dbTrade.tp1SizePercent ?? config.takeProfit1SizePercent, tp2SizePercent: pmState?.tp2SizePercent ?? dbTrade.tp2SizePercent ?? config.takeProfit2SizePercent, 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 = `autosync-${now.getTime()}-${driftPos.marketIndex}` // CRITICAL FIX (Dec 12, 2025): Query Drift for existing orders // Auto-synced positions need order signatures so PM can update orders after TP1/TP2 events console.log(`🔍 Querying Drift for existing orders on ${driftPos.symbol}...`) const existingOrders = await discoverExistingOrders(driftPos.symbol, 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: 'autosync', timeframe: 'sync', // CRITICAL FIX (Dec 12, 2025): Record discovered order signatures tp1OrderTx: existingOrders.tp1OrderTx || null, tp2OrderTx: existingOrders.tp2OrderTx || null, slOrderTx: existingOrders.slOrderTx || null, softStopOrderTx: existingOrders.softStopOrderTx || null, hardStopOrderTx: existingOrders.hardStopOrderTx || null, configSnapshot: { source: 'health-monitor-autosync', syncedAt: now.toISOString(), discoveredOrders: existingOrders, // Store for debugging positionManagerState: { currentSize: positionSizeUSD, tp1Hit: false, tp2Hit: false, slMovedToBreakeven: false, slMovedToProfit: false, trailingStopActive: false, stopLossPrice, realizedPnL: 0, unrealizedPnL: driftPos.unrealizedPnL ?? 0, peakPnL: driftPos.unrealizedPnL ?? 0, peakPrice: entryPrice, lastPrice: currentPrice, maxFavorableExcursion: 0, maxAdverseExcursion: 0, maxFavorablePrice: entryPrice, maxAdversePrice: entryPrice, lastUpdate: now.toISOString(), // TP2 runner configuration (CRITICAL FIX - Dec 12, 2025) tp1SizePercent: config.takeProfit1SizePercent, tp2SizePercent: config.takeProfit2SizePercent, trailingStopDistance: undefined, runnerTrailingPercent: undefined, }, }, entryOrderTx: syntheticPositionId, }, }) activeTrade = buildActiveTradeFromDb(placeholderTrade) } await positionManager.addTrade(activeTrade) console.log(`✅ Synced ${driftPos.symbol} to Position Manager`) } catch (error) { console.error(`❌ Failed to sync ${driftPos.symbol}:`, error) throw error } }