diff --git a/app/analytics/optimization/page.tsx b/app/analytics/optimization/page.tsx index 5ffb5c8..2d09bb5 100644 --- a/app/analytics/optimization/page.tsx +++ b/app/analytics/optimization/page.tsx @@ -11,15 +11,25 @@ interface AnalysisResult { action?: string } +// Available indicator versions for filtering +const INDICATOR_VERSIONS = [ + { value: 'v9', label: 'v9 - Current Production' }, + { value: 'v8', label: 'v8 - Previous Version' }, + { value: 'v6', label: 'v6 - Legacy' }, + { value: 'v5', label: 'v5 - Legacy' }, + { value: 'all', label: 'All Versions (Historical)' }, +] + export default function OptimizationPage() { const [analyses, setAnalyses] = useState([]) const [loading, setLoading] = useState(true) const [lastRefresh, setLastRefresh] = useState(null) + const [selectedVersion, setSelectedVersion] = useState('v9') // Default to current production const loadAnalyses = async () => { setLoading(true) try { - const response = await fetch('/api/optimization/analyze') + const response = await fetch(`/api/optimization/analyze?version=${encodeURIComponent(selectedVersion)}`) const data = await response.json() setAnalyses(data.analyses) setLastRefresh(new Date()) @@ -32,7 +42,8 @@ export default function OptimizationPage() { useEffect(() => { loadAnalyses() - }, []) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedVersion]) // Reload when version changes return (
@@ -60,8 +71,25 @@ export default function OptimizationPage() { )}
- {/* Refresh Button */} -
+ {/* Version Selector and Refresh Button */} +
+
+ + +
+ {selectedVersion !== 'all' && ( + + ✓ Filtering by {selectedVersion.toUpperCase()} + + )}
{/* Loading State */} diff --git a/app/api/optimization/analyze/route.ts b/app/api/optimization/analyze/route.ts index 33df37a..0260644 100644 --- a/app/api/optimization/analyze/route.ts +++ b/app/api/optimization/analyze/route.ts @@ -1,41 +1,86 @@ -import { NextResponse } from 'next/server' +import { NextRequest, NextResponse } from 'next/server' import { getPrismaClient } from '../../../../lib/database/trades' +import { Prisma } from '@prisma/client' export const dynamic = 'force-dynamic' -export async function GET() { +// 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 { - const qualityDistribution = 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 - ` + // 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, @@ -53,12 +98,12 @@ export async function GET() { analyses.push({ name: 'Quality Score Distribution', - description: 'Win rate and P&L across quality score tiers', + description: `Win rate and P&L across quality score tiers (${versionLabel})`, status: 'success', data: formattedData, recommendation: bestTier.win_rate >= 70 - ? `Quality ${bestTier.tier.split(' ')[0]} shows ${bestTier.win_rate}% WR. Consider raising threshold to this tier.` - : 'Continue collecting data for reliable quality threshold optimization.' + ? `${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({ @@ -73,21 +118,38 @@ export async function GET() { // 2. DIRECTION PERFORMANCE (Long vs Short) // ============================================================================ try { - const directionPerformance = 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 - ` + 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(), @@ -108,15 +170,15 @@ export async function GET() { if (wrDiff > 15) { const better = longData.win_rate > shortData.win_rate ? 'LONG' : 'SHORT' const worse = better === 'LONG' ? 'SHORT' : 'LONG' - recommendation = `${better} signals perform ${wrDiff.toFixed(1)}% better. Consider raising ${worse} quality threshold or reducing ${worse} position size.` + recommendation = `${versionLabel}: ${better} signals perform ${wrDiff.toFixed(1)}% better. Consider raising ${worse} quality threshold or reducing ${worse} position size.` } else { - recommendation = 'Direction performance is balanced. No threshold adjustment needed.' + recommendation = `${versionLabel}: Direction performance is balanced. No threshold adjustment needed.` } } analyses.push({ name: 'Direction Performance', - description: 'Compare LONG vs SHORT trade outcomes', + description: `Compare LONG vs SHORT trade outcomes (${versionLabel})`, status: 'success', data: formattedData, recommendation @@ -134,18 +196,32 @@ export async function GET() { // 3. BLOCKED SIGNALS ANALYSIS // ============================================================================ try { - const blockedSignals = 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 - ` + 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), @@ -158,15 +234,15 @@ export async function GET() { const qualityBlocked = formattedData.find(d => d.reason === 'QUALITY_SCORE_TOO_LOW') let recommendation = '' if (qualityBlocked && qualityBlocked.count >= 20) { - recommendation = `${qualityBlocked.count} signals blocked by quality threshold. Ready for Phase 2 analysis - check if these would have been profitable.` + 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 = `Need ${needed} more blocked signals for reliable analysis. Keep collecting data.` + 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', + description: `Signals rejected by quality filters (${versionLabel})`, status: 'success', data: formattedData, recommendation, @@ -187,43 +263,60 @@ export async function GET() { // 4. RUNNER PERFORMANCE ANALYSIS // ============================================================================ try { - const runnerPerformance = 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) / COUNT(*)::numeric, 1) as tp1_rate, - ROUND(100.0 * SUM(CASE WHEN "tp2Filled" = true THEN 1 ELSE 0 END) / COUNT(*)::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' - ` + 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), - tp1_hits: Number(data.tp1_hits), - tp2_hits: Number(data.tp2_hits), - tp1_rate: Number(data.tp1_rate), - tp2_rate: Number(data.tp2_rate), - avg_mfe: Number(data.avg_mfe), - avg_mae: Number(data.avg_mae) + 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.avg_mfe > formattedData.tp1_rate * 1.5) { - recommendation = `Avg MFE (${formattedData.avg_mfe.toFixed(2)}%) significantly exceeds TP1 rate. Consider widening TP1 or increasing runner size.` + 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 = `TP2 hit rate is ${formattedData.tp2_rate}%. Trailing stop working well - keep current settings.` + recommendation = `${versionLabel}: TP2 hit rate is ${formattedData.tp2_rate}%. Trailing stop working well - keep current settings.` } else { - recommendation = 'Runner performance is within expected range. Continue monitoring.' + recommendation = `${versionLabel}: Runner performance is within expected range. Continue monitoring.` } analyses.push({ name: 'Runner Performance', - description: 'TP1/TP2 hit rates and max excursion analysis', + description: `TP1/TP2 hit rates and max excursion analysis (${versionLabel})`, status: 'success', data: formattedData, recommendation @@ -241,24 +334,44 @@ export async function GET() { // 5. ATR CORRELATION WITH MFE // ============================================================================ try { - const atrCorrelation = 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") - ` + 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), @@ -268,17 +381,19 @@ export async function GET() { win_rate: Number(row.win_rate) })) - let recommendation = 'ATR-based targets already implemented. Monitor correlation over time.' + 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 = `${highestMFE.atr_range} shows highest avg MFE (${highestMFE.avg_mfe}%). ATR-based targets adapting correctly.` + 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', + description: `How volatility affects profit potential (${versionLabel})`, status: 'success', data: formattedData, recommendation @@ -352,45 +467,59 @@ export async function GET() { // 7. DATA COLLECTION STATUS // ============================================================================ try { - const dataStatus = 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" = 'v8') as v8_trades, - (SELECT COUNT(*) FROM "BlockedSignal" WHERE "blockReason" = 'QUALITY_SCORE_TOO_LOW') as blocked_quality - FROM "Trade" - ` + // 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), - with_quality: Number(data.with_quality), - with_atr: Number(data.with_atr), - with_mfe: Number(data.with_mfe), - v8_trades: Number(data.v8_trades), - blocked_quality: Number(data.blocked_quality) + 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 v8Needed = Math.max(0, 50 - formattedData.v8_trades) + const versionNeeded = Math.max(0, 50 - formattedData.version_trades) + const targetVersion = version === 'all' ? 'v9' : version let action = '' if (blockedNeeded > 0) { - action = `Need ${blockedNeeded} more blocked signals for Phase 2 quality threshold analysis.` + action = `${versionLabel}: Need ${blockedNeeded} more blocked signals for Phase 2 quality threshold analysis.` } - if (v8Needed > 0) { + if (versionNeeded > 0) { if (action) action += ' ' - action += `Need ${v8Needed} more v8 indicator trades for statistical validation.` + action += `Need ${versionNeeded} more ${targetVersion} indicator trades for statistical validation.` } analyses.push({ name: 'Data Collection Status', - description: 'Progress toward analysis milestones', + description: `Progress toward analysis milestones (${versionLabel})`, status: 'success', data: formattedData, - recommendation: action || 'Data collection milestones reached! Ready for optimization decisions.', + recommendation: action || `${versionLabel}: Data collection milestones reached! Ready for optimization decisions.`, action: action || undefined }) } catch (error) { @@ -405,6 +534,7 @@ export async function GET() { return NextResponse.json({ success: true, analyses, + selectedVersion: version, timestamp: new Date().toISOString() }) diff --git a/package-lock.json b/package-lock.json index 7ece840..3225e22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12951,7 +12951,6 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc",