import { NextRequest, NextResponse } from 'next/server' import { getPrismaClient } from '../../../../lib/database/trades' import { Prisma } from '@prisma/client' export const dynamic = 'force-dynamic' // Valid indicator versions for filtering const VALID_VERSIONS = ['v5', 'v6', 'v8', 'v9', 'all'] export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const version = searchParams.get('version') || 'v9' // Default to current production // Validate version parameter to prevent SQL injection if (!VALID_VERSIONS.includes(version)) { return NextResponse.json({ success: false, error: `Invalid version parameter. Must be one of: ${VALID_VERSIONS.join(', ')}` }, { status: 400 }) } const prisma = getPrismaClient() const analyses = [] // Version label for recommendations const versionLabel = version === 'all' ? 'All versions combined' : `${version.toUpperCase()} only` try { // ============================================================================ // 1. QUALITY SCORE DISTRIBUTION // ============================================================================ try { // Build query with optional version filter const qualityDistribution = version === 'all' ? await prisma.$queryRaw` SELECT CASE WHEN "signalQualityScore" >= 95 THEN '95-100 (Excellent)' WHEN "signalQualityScore" >= 90 THEN '90-94 (Very Good)' WHEN "signalQualityScore" >= 85 THEN '85-89 (Good)' WHEN "signalQualityScore" >= 80 THEN '80-84 (Fair)' WHEN "signalQualityScore" >= 70 THEN '70-79 (Marginal)' WHEN "signalQualityScore" >= 60 THEN '60-69 (Weak)' ELSE '<60 (Very Weak)' END as tier, COUNT(*) as trades, ROUND(AVG("signalQualityScore")::numeric, 1) as avg_score, SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) as wins, SUM(CASE WHEN "realizedPnL" <= 0 THEN 1 ELSE 0 END) as losses, ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate, ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl FROM "Trade" WHERE "exitReason" IS NOT NULL AND "signalQualityScore" IS NOT NULL GROUP BY tier ORDER BY MIN("signalQualityScore") DESC ` : await prisma.$queryRaw` SELECT CASE WHEN "signalQualityScore" >= 95 THEN '95-100 (Excellent)' WHEN "signalQualityScore" >= 90 THEN '90-94 (Very Good)' WHEN "signalQualityScore" >= 85 THEN '85-89 (Good)' WHEN "signalQualityScore" >= 80 THEN '80-84 (Fair)' WHEN "signalQualityScore" >= 70 THEN '70-79 (Marginal)' WHEN "signalQualityScore" >= 60 THEN '60-69 (Weak)' ELSE '<60 (Very Weak)' END as tier, COUNT(*) as trades, ROUND(AVG("signalQualityScore")::numeric, 1) as avg_score, SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) as wins, SUM(CASE WHEN "realizedPnL" <= 0 THEN 1 ELSE 0 END) as losses, ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate, ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl FROM "Trade" WHERE "exitReason" IS NOT NULL AND "signalQualityScore" IS NOT NULL AND "indicatorVersion" = ${version} GROUP BY tier ORDER BY MIN("signalQualityScore") DESC ` const formattedData = qualityDistribution.map(row => ({ tier: row.tier, trades: Number(row.trades), avg_score: Number(row.avg_score), win_rate: Number(row.win_rate), total_pnl: Number(row.total_pnl), avg_pnl: Number(row.avg_pnl) })) // Find best performing tier const bestTier = formattedData.reduce((best, current) => current.win_rate > best.win_rate ? current : best ) analyses.push({ name: 'Quality Score Distribution', description: `Win rate and P&L across quality score tiers (${versionLabel})`, status: 'success', data: formattedData, recommendation: bestTier.win_rate >= 70 ? `${versionLabel}: Quality ${bestTier.tier.split(' ')[0]} shows ${bestTier.win_rate}% WR. Consider raising threshold to this tier.` : `${versionLabel}: Continue collecting data for reliable quality threshold optimization.` }) } catch (error) { analyses.push({ name: 'Quality Score Distribution', description: 'Win rate and P&L across quality score tiers', status: 'error', data: { error: (error as Error).message } }) } // ============================================================================ // 2. DIRECTION PERFORMANCE (Long vs Short) // ============================================================================ try { const directionPerformance = version === 'all' ? await prisma.$queryRaw` SELECT direction, COUNT(*) as trades, SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) as wins, ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate, ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, ROUND(AVG("signalQualityScore")::numeric, 1) as avg_quality FROM "Trade" WHERE "exitReason" IS NOT NULL AND direction IS NOT NULL GROUP BY direction ORDER BY win_rate DESC ` : await prisma.$queryRaw` SELECT direction, COUNT(*) as trades, SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) as wins, ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate, ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, ROUND(AVG("signalQualityScore")::numeric, 1) as avg_quality FROM "Trade" WHERE "exitReason" IS NOT NULL AND direction IS NOT NULL AND "indicatorVersion" = ${version} GROUP BY direction ORDER BY win_rate DESC ` const formattedData = directionPerformance.map(row => ({ direction: String(row.direction).toUpperCase(), trades: Number(row.trades), wins: Number(row.wins), win_rate: Number(row.win_rate), total_pnl: Number(row.total_pnl), avg_pnl: Number(row.avg_pnl), avg_quality: Number(row.avg_quality) })) const longData = formattedData.find(d => d.direction === 'LONG') const shortData = formattedData.find(d => d.direction === 'SHORT') let recommendation = '' if (longData && shortData) { const wrDiff = Math.abs(longData.win_rate - shortData.win_rate) if (wrDiff > 15) { const better = longData.win_rate > shortData.win_rate ? 'LONG' : 'SHORT' const worse = better === 'LONG' ? 'SHORT' : 'LONG' recommendation = `${versionLabel}: ${better} signals perform ${wrDiff.toFixed(1)}% better. Consider raising ${worse} quality threshold or reducing ${worse} position size.` } else { recommendation = `${versionLabel}: Direction performance is balanced. No threshold adjustment needed.` } } analyses.push({ name: 'Direction Performance', description: `Compare LONG vs SHORT trade outcomes (${versionLabel})`, status: 'success', data: formattedData, recommendation }) } catch (error) { analyses.push({ name: 'Direction Performance', description: 'Compare LONG vs SHORT trade outcomes', status: 'error', data: { error: (error as Error).message } }) } // ============================================================================ // 3. BLOCKED SIGNALS ANALYSIS // ============================================================================ try { const blockedSignals = version === 'all' ? await prisma.$queryRaw` SELECT "blockReason", COUNT(*) as count, ROUND(AVG("signalQualityScore")::numeric, 1) as avg_score, ROUND(AVG(adx)::numeric, 1) as avg_adx, ROUND(AVG(atr)::numeric, 3) as avg_atr FROM "BlockedSignal" WHERE "blockReason" IS NOT NULL GROUP BY "blockReason" ORDER BY count DESC ` : await prisma.$queryRaw` SELECT "blockReason", COUNT(*) as count, ROUND(AVG("signalQualityScore")::numeric, 1) as avg_score, ROUND(AVG(adx)::numeric, 1) as avg_adx, ROUND(AVG(atr)::numeric, 3) as avg_atr FROM "BlockedSignal" WHERE "blockReason" IS NOT NULL AND "indicatorVersion" = ${version} GROUP BY "blockReason" ORDER BY count DESC ` const formattedData = blockedSignals.map(row => ({ reason: String(row.blockReason), count: Number(row.count), avg_score: Number(row.avg_score), avg_adx: Number(row.avg_adx), avg_atr: Number(row.avg_atr) })) const qualityBlocked = formattedData.find(d => d.reason === 'QUALITY_SCORE_TOO_LOW') let recommendation = '' if (qualityBlocked && qualityBlocked.count >= 20) { recommendation = `${versionLabel}: ${qualityBlocked.count} signals blocked by quality threshold. Ready for Phase 2 analysis - check if these would have been profitable.` } else { const needed = qualityBlocked ? 20 - qualityBlocked.count : 20 recommendation = `${versionLabel}: Need ${needed} more blocked signals for reliable analysis. Keep collecting data.` } analyses.push({ name: 'Blocked Signals Analysis', description: `Signals rejected by quality filters (${versionLabel})`, status: 'success', data: formattedData, recommendation, action: qualityBlocked && qualityBlocked.count >= 20 ? 'Run price tracking analysis to determine if blocked signals would have been profitable' : undefined }) } catch (error) { analyses.push({ name: 'Blocked Signals Analysis', description: 'Signals rejected by quality filters', status: 'error', data: { error: (error as Error).message } }) } // ============================================================================ // 4. RUNNER PERFORMANCE ANALYSIS // ============================================================================ try { const runnerPerformance = version === 'all' ? await prisma.$queryRaw` SELECT COUNT(*) as total_trades, SUM(CASE WHEN "tp1Filled" = true THEN 1 ELSE 0 END) as tp1_hits, SUM(CASE WHEN "tp2Filled" = true THEN 1 ELSE 0 END) as tp2_hits, ROUND(100.0 * SUM(CASE WHEN "tp1Filled" = true THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)::numeric, 1) as tp1_rate, ROUND(100.0 * SUM(CASE WHEN "tp2Filled" = true THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)::numeric, 1) as tp2_rate, ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_mae FROM "Trade" WHERE "exitReason" IS NOT NULL AND "createdAt" >= NOW() - INTERVAL '30 days' ` : await prisma.$queryRaw` SELECT COUNT(*) as total_trades, SUM(CASE WHEN "tp1Filled" = true THEN 1 ELSE 0 END) as tp1_hits, SUM(CASE WHEN "tp2Filled" = true THEN 1 ELSE 0 END) as tp2_hits, ROUND(100.0 * SUM(CASE WHEN "tp1Filled" = true THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)::numeric, 1) as tp1_rate, ROUND(100.0 * SUM(CASE WHEN "tp2Filled" = true THEN 1 ELSE 0 END) / NULLIF(COUNT(*), 0)::numeric, 1) as tp2_rate, ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_mae FROM "Trade" WHERE "exitReason" IS NOT NULL AND "createdAt" >= NOW() - INTERVAL '30 days' AND "indicatorVersion" = ${version} ` const data = runnerPerformance[0] const formattedData = { total_trades: Number(data.total_trades) || 0, tp1_hits: Number(data.tp1_hits) || 0, tp2_hits: Number(data.tp2_hits) || 0, tp1_rate: Number(data.tp1_rate) || 0, tp2_rate: Number(data.tp2_rate) || 0, avg_mfe: Number(data.avg_mfe) || 0, avg_mae: Number(data.avg_mae) || 0 } let recommendation = '' if (formattedData.total_trades === 0) { recommendation = `${versionLabel}: No trades found in last 30 days. Need data to analyze runner performance.` } else if (formattedData.avg_mfe > formattedData.tp1_rate * 1.5) { recommendation = `${versionLabel}: Avg MFE (${formattedData.avg_mfe.toFixed(2)}%) significantly exceeds TP1 rate. Consider widening TP1 or increasing runner size.` } else if (formattedData.tp2_rate > 50) { recommendation = `${versionLabel}: TP2 hit rate is ${formattedData.tp2_rate}%. Trailing stop working well - keep current settings.` } else { recommendation = `${versionLabel}: Runner performance is within expected range. Continue monitoring.` } analyses.push({ name: 'Runner Performance', description: `TP1/TP2 hit rates and max excursion analysis (${versionLabel})`, status: 'success', data: formattedData, recommendation }) } catch (error) { analyses.push({ name: 'Runner Performance', description: 'TP1/TP2 hit rates and max excursion analysis', status: 'error', data: { error: (error as Error).message } }) } // ============================================================================ // 5. ATR CORRELATION WITH MFE // ============================================================================ try { const atrCorrelation = version === 'all' ? await prisma.$queryRaw` SELECT CASE WHEN "atrAtEntry" < 0.3 THEN '<0.3 (Low Vol)' WHEN "atrAtEntry" < 0.5 THEN '0.3-0.5 (Med Vol)' WHEN "atrAtEntry" < 0.7 THEN '0.5-0.7 (High Vol)' ELSE '0.7+ (Very High Vol)' END as atr_range, COUNT(*) as trades, ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate FROM "Trade" WHERE "exitReason" IS NOT NULL AND "atrAtEntry" IS NOT NULL GROUP BY atr_range ORDER BY MIN("atrAtEntry") ` : await prisma.$queryRaw` SELECT CASE WHEN "atrAtEntry" < 0.3 THEN '<0.3 (Low Vol)' WHEN "atrAtEntry" < 0.5 THEN '0.3-0.5 (Med Vol)' WHEN "atrAtEntry" < 0.7 THEN '0.5-0.7 (High Vol)' ELSE '0.7+ (Very High Vol)' END as atr_range, COUNT(*) as trades, ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate FROM "Trade" WHERE "exitReason" IS NOT NULL AND "atrAtEntry" IS NOT NULL AND "indicatorVersion" = ${version} GROUP BY atr_range ORDER BY MIN("atrAtEntry") ` const formattedData = atrCorrelation.map(row => ({ atr_range: String(row.atr_range), trades: Number(row.trades), avg_mfe: Number(row.avg_mfe), avg_pnl: Number(row.avg_pnl), win_rate: Number(row.win_rate) })) let recommendation = `${versionLabel}: ATR-based targets already implemented. Monitor correlation over time.` if (formattedData.length >= 3) { const highestMFE = formattedData.reduce((best, current) => current.avg_mfe > best.avg_mfe ? current : best ) recommendation = `${versionLabel}: ${highestMFE.atr_range} shows highest avg MFE (${highestMFE.avg_mfe}%). ATR-based targets adapting correctly.` } else if (formattedData.length === 0) { recommendation = `${versionLabel}: No ATR data available. Need trades with ATR tracking enabled.` } analyses.push({ name: 'ATR vs MFE Correlation', description: `How volatility affects profit potential (${versionLabel})`, status: 'success', data: formattedData, recommendation }) } catch (error) { analyses.push({ name: 'ATR vs MFE Correlation', description: 'How volatility affects profit potential', status: 'error', data: { error: (error as Error).message } }) } // ============================================================================ // 6. INDICATOR VERSION COMPARISON // ============================================================================ try { const indicatorComparison = await prisma.$queryRaw` SELECT "indicatorVersion", COUNT(*) as trades, SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) as wins, ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate, ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, ROUND(AVG("signalQualityScore")::numeric, 1) as avg_quality FROM "Trade" WHERE "exitReason" IS NOT NULL AND "indicatorVersion" IS NOT NULL GROUP BY "indicatorVersion" ORDER BY "indicatorVersion" DESC ` const formattedData = indicatorComparison.map(row => ({ version: String(row.indicatorVersion), trades: Number(row.trades), wins: Number(row.wins), win_rate: Number(row.win_rate), total_pnl: Number(row.total_pnl), avg_pnl: Number(row.avg_pnl), avg_quality: Number(row.avg_quality) })) const v8Data = formattedData.find(d => d.version === 'v8') let recommendation = '' if (v8Data && v8Data.trades >= 20) { recommendation = `v8 has ${v8Data.trades} trades with ${v8Data.win_rate}% WR. Sufficient data for statistical confidence.` } else if (v8Data) { recommendation = `v8 has ${v8Data.trades} trades. Need ${20 - v8Data.trades} more for statistical validation.` } else { recommendation = 'No v8 indicator data yet. Ensure TradingView alerts include IND:v8 field.' } analyses.push({ name: 'Indicator Version Comparison', description: 'Performance across TradingView strategy versions', status: 'success', data: formattedData, recommendation }) } catch (error) { analyses.push({ name: 'Indicator Version Comparison', description: 'Performance across TradingView strategy versions', status: 'error', data: { error: (error as Error).message } }) } // ============================================================================ // 7. DATA COLLECTION STATUS // ============================================================================ try { // For 'all' version, show overall data collection status // For specific version, show that version's data status const dataStatus = version === 'all' ? await prisma.$queryRaw` SELECT COUNT(*) FILTER (WHERE "exitReason" IS NOT NULL) as completed_trades, COUNT(*) FILTER (WHERE "signalQualityScore" IS NOT NULL) as with_quality, COUNT(*) FILTER (WHERE "atrAtEntry" IS NOT NULL) as with_atr, COUNT(*) FILTER (WHERE "maxFavorableExcursion" IS NOT NULL) as with_mfe, COUNT(*) FILTER (WHERE "indicatorVersion" = 'v9') as version_trades, (SELECT COUNT(*) FROM "BlockedSignal" WHERE "blockReason" = 'QUALITY_SCORE_TOO_LOW') as blocked_quality FROM "Trade" ` : await prisma.$queryRaw` SELECT COUNT(*) FILTER (WHERE "exitReason" IS NOT NULL AND "indicatorVersion" = ${version}) as completed_trades, COUNT(*) FILTER (WHERE "signalQualityScore" IS NOT NULL AND "indicatorVersion" = ${version}) as with_quality, COUNT(*) FILTER (WHERE "atrAtEntry" IS NOT NULL AND "indicatorVersion" = ${version}) as with_atr, COUNT(*) FILTER (WHERE "maxFavorableExcursion" IS NOT NULL AND "indicatorVersion" = ${version}) as with_mfe, COUNT(*) FILTER (WHERE "indicatorVersion" = ${version}) as version_trades, (SELECT COUNT(*) FROM "BlockedSignal" WHERE "blockReason" = 'QUALITY_SCORE_TOO_LOW' AND "indicatorVersion" = ${version}) as blocked_quality FROM "Trade" ` const data = dataStatus[0] const formattedData = { completed_trades: Number(data.completed_trades) || 0, with_quality: Number(data.with_quality) || 0, with_atr: Number(data.with_atr) || 0, with_mfe: Number(data.with_mfe) || 0, version_trades: Number(data.version_trades) || 0, blocked_quality: Number(data.blocked_quality) || 0 } const blockedNeeded = Math.max(0, 20 - formattedData.blocked_quality) const versionNeeded = Math.max(0, 50 - formattedData.version_trades) const targetVersion = version === 'all' ? 'v9' : version let action = '' if (blockedNeeded > 0) { action = `${versionLabel}: Need ${blockedNeeded} more blocked signals for Phase 2 quality threshold analysis.` } if (versionNeeded > 0) { if (action) action += ' ' action += `Need ${versionNeeded} more ${targetVersion} indicator trades for statistical validation.` } analyses.push({ name: 'Data Collection Status', description: `Progress toward analysis milestones (${versionLabel})`, status: 'success', data: formattedData, recommendation: action || `${versionLabel}: Data collection milestones reached! Ready for optimization decisions.`, action: action || undefined }) } catch (error) { analyses.push({ name: 'Data Collection Status', description: 'Progress toward analysis milestones', status: 'error', data: { error: (error as Error).message } }) } return NextResponse.json({ success: true, analyses, selectedVersion: version, timestamp: new Date().toISOString() }) } catch (error) { console.error('❌ Optimization analysis failed:', error) return NextResponse.json({ success: false, error: 'Failed to run optimization analyses', message: (error as Error).message }, { status: 500 }) } }