/** * Database Views and Analytics Queries * * Provides net position calculations and analytics that match what you see on Drift */ import { getPrismaClient } from './trades' /** * Get net positions across all open trades (matches Drift UI) * * NOTE: Drift perpetuals NET opposite positions in the same market. * If you have both LONG and SHORT positions in SOL-PERP, Drift shows only the net exposure. */ export async function getNetPositions() { const prisma = getPrismaClient() const openTrades = await prisma.trade.findMany({ where: { status: 'open', isTestTrade: false, // Exclude test trades }, select: { symbol: true, direction: true, positionSizeUSD: true, entryPrice: true, leverage: true, }, }) // Group by symbol and calculate net positions const netBySymbol = new Map() for (const trade of openTrades) { const existing = netBySymbol.get(trade.symbol) || { symbol: trade.symbol, longUSD: 0, shortUSD: 0, longSOL: 0, shortSOL: 0, netUSD: 0, netSOL: 0, netDirection: 'flat' as const, avgLongEntry: 0, avgShortEntry: 0, tradeCount: 0, } const solSize = trade.positionSizeUSD / trade.entryPrice if (trade.direction === 'long') { existing.longUSD += trade.positionSizeUSD existing.longSOL += solSize existing.avgLongEntry = existing.longUSD / existing.longSOL } else { existing.shortUSD += trade.positionSizeUSD existing.shortSOL += solSize existing.avgShortEntry = existing.shortUSD / existing.shortSOL } existing.tradeCount++ netBySymbol.set(trade.symbol, existing) } // Calculate net exposure const results = [] for (const [symbol, data] of netBySymbol) { data.netSOL = data.longSOL - data.shortSOL data.netUSD = data.longUSD - data.shortUSD if (Math.abs(data.netSOL) < 0.001) { data.netDirection = 'flat' } else { data.netDirection = data.netSOL > 0 ? 'long' : 'short' } results.push(data) } return results } /** * Get individual trades with their contribution to net position */ export async function getTradesWithNetContext() { const prisma = getPrismaClient() const trades = await prisma.trade.findMany({ where: { status: 'open', isTestTrade: false, }, orderBy: { createdAt: 'desc', }, }) const netPositions = await getNetPositions() const netMap = new Map(netPositions.map(n => [n.symbol, n])) return trades.map(trade => { const net = netMap.get(trade.symbol) const solSize = trade.positionSizeUSD / trade.entryPrice return { ...trade, solSize, netPosition: net ? { netSOL: net.netSOL, netUSD: net.netUSD, netDirection: net.netDirection, contributionPercent: Math.abs((solSize / net.netSOL) * 100), } : null, } }) } /** * Get trading statistics (excludes test trades) */ export async function getTradingStats(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, // Real trades only OR: [ { signalSource: null }, // Old trades without signalSource { signalSource: { not: 'manual' } }, // Exclude manual Telegram trades ], }, }) const testTrades = await prisma.trade.count({ where: { createdAt: { gte: since }, isTestTrade: true, }, }) 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 const profitFactor = avgLoss !== 0 ? avgWin / Math.abs(avgLoss) : 0 return { period: `Last ${days} days`, realTrades: { total: trades.length, winning: winning.length, losing: losing.length, winRate: `${winRate.toFixed(1)}%`, totalPnL: `$${totalPnL.toFixed(2)}`, avgWin: `$${avgWin.toFixed(2)}`, avgLoss: `$${avgLoss.toFixed(2)}`, profitFactor: profitFactor.toFixed(2), }, testTrades: { count: testTrades, note: 'Excluded from statistics above', }, } } /** * Get position summary (what you see on Drift vs what's in database) */ export async function getPositionSummary() { const prisma = getPrismaClient() const openTrades = await prisma.trade.count({ where: { status: 'open', isTestTrade: false }, }) const openTestTrades = await prisma.trade.count({ where: { status: 'open', isTestTrade: true }, }) const individualPositions = await prisma.trade.findMany({ where: { status: 'open', isTestTrade: false }, select: { symbol: true, direction: true, positionSizeUSD: true, entryPrice: true, }, }) const totalIndividualUSD = individualPositions.reduce( (sum, t) => sum + t.positionSizeUSD, 0 ) const netPositions = await getNetPositions() const totalNetUSD = netPositions.reduce( (sum, n) => sum + Math.abs(n.netUSD), 0 ) return { summary: { individualTrades: openTrades, testTrades: openTestTrades, totalIndividualExposure: `$${totalIndividualUSD.toFixed(2)}`, netExposure: `$${totalNetUSD.toFixed(2)}`, explanation: 'Drift shows NET exposure (opposite positions cancel out)', }, netPositions, individualPositions, } }