Files
trading_bot_v4/lib/database/views.ts
mindesbunister 25776413d0 feat: Add signalSource field to identify manual vs TradingView trades
- Set signalSource='manual' for Telegram trades, 'tradingview' for TradingView
- Updated analytics queries to exclude manual trades from indicator analysis
- getTradingStats() filters manual trades (TradingView performance only)
- Version comparison endpoint filters manual trades
- Created comprehensive filtering guide: docs/MANUAL_TRADE_FILTERING.md
- Ensures clean data for indicator optimization without contamination
2025-11-14 22:55:14 +01:00

240 lines
6.1 KiB
TypeScript

/**
* 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<string, {
symbol: string
longUSD: number
shortUSD: number
longSOL: number
shortSOL: number
netUSD: number
netSOL: number
netDirection: 'long' | 'short' | 'flat'
avgLongEntry: number
avgShortEntry: number
tradeCount: number
}>()
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,
}
}