/** * Database Service for Trade Tracking and Analytics */ import { PrismaClient } from '@prisma/client' // Singleton Prisma client let prisma: PrismaClient | null = null export function getPrismaClient(): PrismaClient { if (!prisma) { prisma = new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], }) console.log('✅ Prisma client initialized') } return prisma } export interface CreateTradeParams { positionId: string symbol: string direction: 'long' | 'short' entryPrice: number entrySlippage?: number positionSizeUSD: number leverage: number stopLossPrice: number softStopPrice?: number hardStopPrice?: number takeProfit1Price: number takeProfit2Price: number tp1SizePercent: number tp2SizePercent: number entryOrderTx: string tp1OrderTx?: string tp2OrderTx?: string slOrderTx?: string softStopOrderTx?: string hardStopOrderTx?: string configSnapshot: any signalSource?: string signalStrength?: string timeframe?: string isTestTrade?: boolean // Market context fields expectedEntryPrice?: number fundingRateAtEntry?: number atrAtEntry?: number adxAtEntry?: number rsiAtEntry?: number volumeAtEntry?: number pricePositionAtEntry?: number signalQualityScore?: number } export interface UpdateTradeStateParams { positionId: string currentSize: number tp1Hit: boolean slMovedToBreakeven: boolean slMovedToProfit: boolean stopLossPrice: number realizedPnL: number unrealizedPnL: number peakPnL: number lastPrice: number maxFavorableExcursion?: number maxAdverseExcursion?: number maxFavorablePrice?: number maxAdversePrice?: number } export interface UpdateTradeExitParams { positionId: string exitPrice: number exitReason: 'TP1' | 'TP2' | 'SL' | 'SOFT_SL' | 'HARD_SL' | 'manual' | 'emergency' realizedPnL: number exitOrderTx: string holdTimeSeconds: number maxDrawdown?: number maxGain?: number // MAE/MFE final values maxFavorableExcursion?: number maxAdverseExcursion?: number maxFavorablePrice?: number maxAdversePrice?: number } export async function createTrade(params: CreateTradeParams) { const prisma = getPrismaClient() try { // Calculate entry slippage if expected price provided let entrySlippagePct: number | undefined if (params.expectedEntryPrice && params.entrySlippage !== undefined) { entrySlippagePct = params.entrySlippage } const trade = await prisma.trade.create({ data: { positionId: params.positionId, symbol: params.symbol, direction: params.direction, entryPrice: params.entryPrice, entryTime: new Date(), entrySlippage: params.entrySlippage, positionSizeUSD: params.positionSizeUSD, leverage: params.leverage, stopLossPrice: params.stopLossPrice, softStopPrice: params.softStopPrice, hardStopPrice: params.hardStopPrice, takeProfit1Price: params.takeProfit1Price, takeProfit2Price: params.takeProfit2Price, tp1SizePercent: params.tp1SizePercent, tp2SizePercent: params.tp2SizePercent, entryOrderTx: params.entryOrderTx, tp1OrderTx: params.tp1OrderTx, tp2OrderTx: params.tp2OrderTx, slOrderTx: params.slOrderTx, softStopOrderTx: params.softStopOrderTx, hardStopOrderTx: params.hardStopOrderTx, configSnapshot: params.configSnapshot, signalSource: params.signalSource, signalStrength: params.signalStrength, timeframe: params.timeframe, status: 'open', isTestTrade: params.isTestTrade || false, // Market context expectedEntryPrice: params.expectedEntryPrice, entrySlippagePct: entrySlippagePct, fundingRateAtEntry: params.fundingRateAtEntry, atrAtEntry: params.atrAtEntry, adxAtEntry: params.adxAtEntry, rsiAtEntry: params.rsiAtEntry, volumeAtEntry: params.volumeAtEntry, pricePositionAtEntry: params.pricePositionAtEntry, signalQualityScore: params.signalQualityScore, }, }) console.log(`📊 Trade record created: ${trade.id}`) return trade } catch (error) { console.error('❌ Failed to create trade record:', error) throw error } } /** * Update trade when position exits */ export async function updateTradeExit(params: UpdateTradeExitParams) { const prisma = getPrismaClient() try { // First fetch the trade to get positionSizeUSD const existingTrade = await prisma.trade.findUnique({ where: { positionId: params.positionId }, select: { positionSizeUSD: true }, }) if (!existingTrade) { throw new Error(`Trade not found: ${params.positionId}`) } const trade = await prisma.trade.update({ where: { positionId: params.positionId }, data: { exitPrice: params.exitPrice, exitTime: new Date(), exitReason: params.exitReason, realizedPnL: params.realizedPnL, realizedPnLPercent: (params.realizedPnL / existingTrade.positionSizeUSD) * 100, exitOrderTx: params.exitOrderTx, holdTimeSeconds: params.holdTimeSeconds, maxDrawdown: params.maxDrawdown, maxGain: params.maxGain, // Save final MAE/MFE values maxFavorableExcursion: params.maxFavorableExcursion, maxAdverseExcursion: params.maxAdverseExcursion, maxFavorablePrice: params.maxFavorablePrice, maxAdversePrice: params.maxAdversePrice, status: 'closed', }, }) console.log(`📊 Trade closed: ${trade.id} | P&L: $${params.realizedPnL.toFixed(2)}`) return trade } catch (error) { console.error('❌ Failed to update trade exit:', error) throw error } } /** * Update active trade state (for Position Manager persistence) */ export async function updateTradeState(params: UpdateTradeStateParams) { const prisma = getPrismaClient() try { const trade = await prisma.trade.update({ where: { positionId: params.positionId }, data: { // Store Position Manager state in configSnapshot configSnapshot: { ...(await prisma.trade.findUnique({ where: { positionId: params.positionId }, select: { configSnapshot: true } }))?.configSnapshot as any, // Add Position Manager state positionManagerState: { currentSize: params.currentSize, tp1Hit: params.tp1Hit, slMovedToBreakeven: params.slMovedToBreakeven, slMovedToProfit: params.slMovedToProfit, stopLossPrice: params.stopLossPrice, realizedPnL: params.realizedPnL, unrealizedPnL: params.unrealizedPnL, peakPnL: params.peakPnL, lastPrice: params.lastPrice, maxFavorableExcursion: params.maxFavorableExcursion, maxAdverseExcursion: params.maxAdverseExcursion, maxFavorablePrice: params.maxFavorablePrice, maxAdversePrice: params.maxAdversePrice, lastUpdate: new Date().toISOString(), } } }, }) return trade } catch (error) { console.error('❌ Failed to update trade state:', error) // Don't throw - state updates are non-critical } } /** * Get all open trades (for Position Manager recovery) */ export async function getOpenTrades() { const prisma = getPrismaClient() try { const trades = await prisma.trade.findMany({ where: { status: 'open' }, orderBy: { entryTime: 'asc' }, }) console.log(`📊 Found ${trades.length} open trades to restore`) return trades } catch (error) { console.error('❌ Failed to get open trades:', error) return [] } } /** * Get the most recent trade entry time (for cooldown checking) */ export async function getLastTradeTime(): Promise { const prisma = getPrismaClient() try { const lastTrade = await prisma.trade.findFirst({ orderBy: { entryTime: 'desc' }, select: { entryTime: true }, }) return lastTrade?.entryTime || null } catch (error) { console.error('❌ Failed to get last trade time:', error) return null } } /** * Get the most recent trade time for a specific symbol */ export async function getLastTradeTimeForSymbol(symbol: string): Promise { const prisma = getPrismaClient() try { const lastTrade = await prisma.trade.findFirst({ where: { symbol }, orderBy: { entryTime: 'desc' }, select: { entryTime: true }, }) return lastTrade?.entryTime || null } catch (error) { console.error(`❌ Failed to get last trade time for ${symbol}:`, error) return null } } /** * Get the most recent trade with full details */ export async function getLastTrade() { const prisma = getPrismaClient() try { const lastTrade = await prisma.trade.findFirst({ orderBy: { createdAt: 'desc' }, }) return lastTrade } catch (error) { console.error('❌ Failed to get last trade:', error) return null } } /** * Get count of trades in the last hour */ export async function getTradesInLastHour(): Promise { const prisma = getPrismaClient() try { const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000) const count = await prisma.trade.count({ where: { entryTime: { gte: oneHourAgo, }, }, }) return count } catch (error) { console.error('❌ Failed to get trades in last hour:', error) return 0 } } /** * Get total P&L for today */ export async function getTodayPnL(): Promise { const prisma = getPrismaClient() try { const startOfDay = new Date() startOfDay.setHours(0, 0, 0, 0) const result = await prisma.trade.aggregate({ where: { entryTime: { gte: startOfDay, }, status: 'closed', }, _sum: { realizedPnL: true, }, }) return result._sum.realizedPnL || 0 } catch (error) { console.error('❌ Failed to get today PnL:', error) return 0 } } /** * Add price update for a trade (for tracking max gain/drawdown) */ export async function addPriceUpdate( tradeId: string, price: number, pnl: number, pnlPercent: number ) { const prisma = getPrismaClient() try { await prisma.priceUpdate.create({ data: { tradeId, price, pnl, pnlPercent, }, }) } catch (error) { console.error('❌ Failed to add price update:', error) // Don't throw - price updates are non-critical } } /** * Log system event */ export async function logSystemEvent( eventType: string, message: string, details?: any ) { const prisma = getPrismaClient() try { await prisma.systemEvent.create({ data: { eventType, message, details: details ? JSON.parse(JSON.stringify(details)) : null, }, }) } catch (error) { console.error('❌ Failed to log system event:', error) } } /** * Get trade statistics */ export async function getTradeStats(days: number = 30) { const prisma = getPrismaClient() const since = new Date() since.setDate(since.getDate() - days) const trades = await prisma.trade.findMany({ where: { createdAt: { gte: since }, status: 'closed', isTestTrade: false, // Exclude test trades from stats }, }) const winning = trades.filter((t) => (t.realizedPnL ?? 0) > 0) const losing = trades.filter((t) => (t.realizedPnL ?? 0) < 0) const totalPnL = trades.reduce((sum, t) => sum + (t.realizedPnL ?? 0), 0) const winRate = trades.length > 0 ? (winning.length / trades.length) * 100 : 0 const avgWin = winning.length > 0 ? winning.reduce((sum, t) => sum + (t.realizedPnL ?? 0), 0) / winning.length : 0 const avgLoss = losing.length > 0 ? losing.reduce((sum, t) => sum + (t.realizedPnL ?? 0), 0) / losing.length : 0 return { totalTrades: trades.length, winningTrades: winning.length, losingTrades: losing.length, winRate: winRate.toFixed(2), totalPnL: totalPnL.toFixed(2), avgWin: avgWin.toFixed(2), avgLoss: avgLoss.toFixed(2), profitFactor: avgLoss !== 0 ? (avgWin / Math.abs(avgLoss)).toFixed(2) : 'N/A', } } /** * Disconnect Prisma client (for graceful shutdown) */ export async function disconnectPrisma() { if (prisma) { await prisma.$disconnect() prisma = null console.log('✅ Prisma client disconnected') } }