feat: Add comprehensive optimization analytics dashboard
- Created /api/optimization/analyze endpoint with 7 SQL analyses - Replaced old TP/SL page with comprehensive dashboard - Analyses: Quality Score Distribution, Direction Performance, Blocked Signals, Runner Performance, ATR vs MFE, Indicator Versions, Data Collection Status - Real-time refresh capability - Actionable recommendations based on data thresholds - Roadmap links at bottom - Addresses user request for automated SQL analysis dashboard
This commit is contained in:
@@ -1,572 +1,218 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface TPSLAnalysis {
|
||||
success: boolean
|
||||
analysis?: {
|
||||
totalTrades: number
|
||||
winningTrades: number
|
||||
losingTrades: number
|
||||
winRate: number
|
||||
avgWin: number
|
||||
avgLoss: number
|
||||
profitFactor: number
|
||||
|
||||
maeAnalysis: {
|
||||
avgMAE: number
|
||||
medianMAE: number
|
||||
percentile25MAE: number
|
||||
percentile75MAE: number
|
||||
worstMAE: number
|
||||
}
|
||||
|
||||
mfeAnalysis: {
|
||||
avgMFE: number
|
||||
medianMFE: number
|
||||
percentile25MFE: number
|
||||
percentile75MFE: number
|
||||
bestMFE: number
|
||||
}
|
||||
|
||||
currentLevels: {
|
||||
tp1Percent: number
|
||||
tp2Percent: number
|
||||
slPercent: number
|
||||
tp1HitRate: number
|
||||
tp2HitRate: number
|
||||
slHitRate: number
|
||||
moneyLeftOnTable: number
|
||||
}
|
||||
|
||||
recommendations: {
|
||||
optimalTP1: number
|
||||
optimalTP2: number
|
||||
optimalSL: number
|
||||
|
||||
reasoning: {
|
||||
tp1: string
|
||||
tp2: string
|
||||
sl: string
|
||||
}
|
||||
|
||||
projectedImpact: {
|
||||
expectedWinRateChange: number
|
||||
expectedProfitFactorChange: number
|
||||
estimatedProfitImprovement: number
|
||||
}
|
||||
}
|
||||
|
||||
tradesByOutcome: {
|
||||
tp1Exits: number
|
||||
tp2Exits: number
|
||||
slExits: number
|
||||
manualExits: number
|
||||
}
|
||||
|
||||
dynamicATRAnalysis?: {
|
||||
available: boolean
|
||||
sampleSize: number
|
||||
minSampleSize: number
|
||||
sufficientData: boolean
|
||||
avgATRPercent: number
|
||||
dynamicTP2Percent: number
|
||||
dynamicSLPercent: number
|
||||
actualPnL: number
|
||||
fixedSimulatedPnL: number
|
||||
dynamicSimulatedPnL: number
|
||||
dynamicAdvantage: number
|
||||
dynamicAdvantagePercent: number
|
||||
dynamicTP2HitRate: number
|
||||
dynamicSLHitRate: number
|
||||
recommendation: string
|
||||
reasoning: string
|
||||
}
|
||||
}
|
||||
error?: string
|
||||
interface AnalysisResult {
|
||||
name: string
|
||||
description: string
|
||||
status: 'loading' | 'success' | 'error'
|
||||
data: any
|
||||
recommendation?: string
|
||||
action?: string
|
||||
}
|
||||
|
||||
export default function OptimizationPage() {
|
||||
const [analysis, setAnalysis] = useState<TPSLAnalysis | null>(null)
|
||||
const [analyses, setAnalyses] = useState<AnalysisResult[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lastRefresh, setLastRefresh] = useState<Date | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
fetchAnalysis()
|
||||
}, [])
|
||||
|
||||
const fetchAnalysis = async () => {
|
||||
try {
|
||||
const loadAnalyses = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const response = await fetch('/api/analytics/tp-sl-optimization')
|
||||
try {
|
||||
const response = await fetch('/api/optimization/analyze')
|
||||
const data = await response.json()
|
||||
|
||||
setAnalysis(data)
|
||||
|
||||
if (!data.success) {
|
||||
setError(data.error || 'Failed to load analysis')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to fetch analytics: ' + (err as Error).message)
|
||||
setAnalyses(data.analyses)
|
||||
setLastRefresh(new Date())
|
||||
} catch (error) {
|
||||
console.error('Failed to load analyses:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
|
||||
<Header />
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
|
||||
<p className="text-gray-400">Loading optimization analysis...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
useEffect(() => {
|
||||
loadAnalyses()
|
||||
}, [])
|
||||
|
||||
if (error || !analysis?.success) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
|
||||
<Header />
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="bg-yellow-900/20 border border-yellow-600 rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold text-yellow-400 mb-2">⚠️ Insufficient Data</h2>
|
||||
<p className="text-gray-300 mb-4">{error || analysis?.error}</p>
|
||||
<p className="text-sm text-gray-400 mb-4">
|
||||
Need at least 10 closed trades with MAE/MFE tracking data.
|
||||
The next trades you take will automatically track this data.
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-4xl font-bold mb-2">🔬 Optimization Analytics Dashboard</h1>
|
||||
<p className="text-slate-400">
|
||||
Data-driven analysis for trading system improvements based on roadmaps and SQL queries
|
||||
</p>
|
||||
<button
|
||||
onClick={fetchAnalysis}
|
||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg font-semibold transition-colors"
|
||||
>
|
||||
🔄 Refresh Analytics
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const data = analysis.analysis!
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
|
||||
<Header />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header with Refresh */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">💡 TP/SL Optimization</h2>
|
||||
<p className="text-sm text-gray-400 mt-1">Based on {data.totalTrades} trades with MAE/MFE data</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchAnalysis}
|
||||
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg transition-colors text-white"
|
||||
>
|
||||
🔄 Refresh
|
||||
</button>
|
||||
{lastRefresh && (
|
||||
<p className="text-sm text-slate-500 mt-2">
|
||||
Last refresh: {lastRefresh.toLocaleTimeString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Overview Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||
<StatCard title="Total Trades" value={data.totalTrades.toString()} />
|
||||
<StatCard
|
||||
title="Win Rate"
|
||||
value={data.winRate.toFixed(1) + '%'}
|
||||
valueColor="text-green-400"
|
||||
/>
|
||||
<StatCard
|
||||
title="Profit Factor"
|
||||
value={data.profitFactor.toFixed(2)}
|
||||
valueColor="text-blue-400"
|
||||
/>
|
||||
<StatCard
|
||||
title="Money Left on Table"
|
||||
value={'$' + data.currentLevels.moneyLeftOnTable.toFixed(2)}
|
||||
valueColor="text-yellow-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* MAE/MFE Analysis */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
|
||||
<h3 className="text-xl font-semibold mb-4 text-green-400">
|
||||
📈 Maximum Favorable Excursion (MFE)
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<MetricRow label="Average" value={data.mfeAnalysis.avgMFE.toFixed(2) + '%'} />
|
||||
<MetricRow label="Median" value={data.mfeAnalysis.medianMFE.toFixed(2) + '%'} />
|
||||
<MetricRow label="25th Percentile" value={data.mfeAnalysis.percentile25MFE.toFixed(2) + '%'} />
|
||||
<MetricRow label="75th Percentile" value={data.mfeAnalysis.percentile75MFE.toFixed(2) + '%'} />
|
||||
<div className="border-t border-gray-700 pt-3">
|
||||
<MetricRow
|
||||
label="Best"
|
||||
value={data.mfeAnalysis.bestMFE.toFixed(2) + '%'}
|
||||
valueColor="text-green-400 font-bold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
|
||||
<h3 className="text-xl font-semibold mb-4 text-red-400">
|
||||
📉 Maximum Adverse Excursion (MAE)
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<MetricRow label="Average" value={data.maeAnalysis.avgMAE.toFixed(2) + '%'} />
|
||||
<MetricRow label="Median" value={data.maeAnalysis.medianMAE.toFixed(2) + '%'} />
|
||||
<MetricRow label="25th Percentile" value={data.maeAnalysis.percentile25MAE.toFixed(2) + '%'} />
|
||||
<MetricRow label="75th Percentile" value={data.maeAnalysis.percentile75MAE.toFixed(2) + '%'} />
|
||||
<div className="border-t border-gray-700 pt-3">
|
||||
<MetricRow
|
||||
label="Worst"
|
||||
value={data.maeAnalysis.worstMAE.toFixed(2) + '%'}
|
||||
valueColor="text-red-400 font-bold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Current Configuration Performance */}
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700 mb-8">
|
||||
<h3 className="text-xl font-semibold mb-6 text-white">🎯 Current Configuration Performance</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<HitRateBar
|
||||
label={'TP1: ' + data.currentLevels.tp1Percent + '%'}
|
||||
hitRate={data.currentLevels.tp1HitRate}
|
||||
exits={data.tradesByOutcome.tp1Exits}
|
||||
color="bg-green-500"
|
||||
/>
|
||||
<HitRateBar
|
||||
label={'TP2: ' + data.currentLevels.tp2Percent + '%'}
|
||||
hitRate={data.currentLevels.tp2HitRate}
|
||||
exits={data.tradesByOutcome.tp2Exits}
|
||||
color="bg-blue-500"
|
||||
/>
|
||||
<HitRateBar
|
||||
label={'SL: ' + data.currentLevels.slPercent + '%'}
|
||||
hitRate={data.currentLevels.slHitRate}
|
||||
exits={data.tradesByOutcome.slExits}
|
||||
color="bg-red-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dynamic ATR Analysis */}
|
||||
{data.dynamicATRAnalysis?.available && (
|
||||
<div className={`rounded-xl p-6 border mb-8 ${
|
||||
data.dynamicATRAnalysis.sufficientData
|
||||
? 'bg-gradient-to-r from-purple-900/30 to-blue-900/30 border-purple-700'
|
||||
: 'bg-yellow-900/20 border-yellow-600'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
🎯 Dynamic ATR-Based TP/SL Analysis
|
||||
</h3>
|
||||
<div className="text-sm text-gray-400">
|
||||
{data.dynamicATRAnalysis.sampleSize}/{data.dynamicATRAnalysis.minSampleSize} trades collected
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{/* Refresh Button */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="text-sm text-gray-400">Data Collection Progress</span>
|
||||
<span className="text-sm font-semibold text-white">
|
||||
{((data.dynamicATRAnalysis.sampleSize / data.dynamicATRAnalysis.minSampleSize) * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-700 rounded-full h-3 overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-500 ${
|
||||
data.dynamicATRAnalysis.sufficientData ? 'bg-green-500' : 'bg-yellow-500'
|
||||
}`}
|
||||
style={{
|
||||
width: Math.min((data.dynamicATRAnalysis.sampleSize / data.dynamicATRAnalysis.minSampleSize) * 100, 100) + '%'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={loadAnalyses}
|
||||
disabled={loading}
|
||||
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 disabled:bg-slate-600 rounded-lg font-medium transition-colors"
|
||||
>
|
||||
{loading ? '🔄 Analyzing...' : '🔄 Refresh All Analyses'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Comparison Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{/* Left: Current Fixed Targets */}
|
||||
<div className="bg-gray-800/50 rounded-lg p-4 border border-gray-700">
|
||||
<h4 className="text-sm font-semibold text-gray-400 mb-3">📌 Fixed Targets (Current)</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-400">TP2:</span>
|
||||
<span className="text-blue-400 font-semibold">{data.currentLevels.tp2Percent}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-400">SL:</span>
|
||||
<span className="text-red-400 font-semibold">{data.currentLevels.slPercent}%</span>
|
||||
</div>
|
||||
<div className="border-t border-gray-700 pt-2 mt-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-400">Simulated P&L:</span>
|
||||
<span className="text-white font-bold">
|
||||
${data.dynamicATRAnalysis.fixedSimulatedPnL.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Dynamic ATR Targets */}
|
||||
<div className="bg-purple-900/30 rounded-lg p-4 border border-purple-600">
|
||||
<h4 className="text-sm font-semibold text-purple-300 mb-3">⚡ Dynamic ATR-Based</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-400">TP2 (2x ATR):</span>
|
||||
<span className="text-blue-400 font-semibold">
|
||||
{data.dynamicATRAnalysis.dynamicTP2Percent.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-400">SL (1.5x ATR):</span>
|
||||
<span className="text-red-400 font-semibold">
|
||||
{data.dynamicATRAnalysis.dynamicSLPercent.toFixed(2)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="border-t border-purple-600 pt-2 mt-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-sm text-gray-400">Simulated P&L:</span>
|
||||
<span className="text-green-400 font-bold">
|
||||
${data.dynamicATRAnalysis.dynamicSimulatedPnL.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advantage Highlight */}
|
||||
<div className={`rounded-lg p-4 mb-4 ${
|
||||
data.dynamicATRAnalysis.dynamicAdvantage >= 0
|
||||
? 'bg-green-900/30 border border-green-600'
|
||||
: 'bg-red-900/30 border border-red-600'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">Dynamic ATR Advantage</div>
|
||||
<div className="text-3xl font-bold text-white">
|
||||
{data.dynamicATRAnalysis.dynamicAdvantage >= 0 ? '+' : ''}
|
||||
${data.dynamicATRAnalysis.dynamicAdvantage.toFixed(2)}
|
||||
<span className="text-xl ml-2 text-gray-400">
|
||||
({data.dynamicATRAnalysis.dynamicAdvantagePercent >= 0 ? '+' : ''}
|
||||
{data.dynamicATRAnalysis.dynamicAdvantagePercent.toFixed(1)}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-400 mb-1">Avg ATR</div>
|
||||
<div className="text-lg font-semibold text-white">
|
||||
{data.dynamicATRAnalysis.avgATRPercent.toFixed(2)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recommendation */}
|
||||
<div className={`rounded-lg p-4 border ${
|
||||
data.dynamicATRAnalysis.sufficientData
|
||||
? data.dynamicATRAnalysis.recommendation.includes('IMPLEMENT')
|
||||
? 'bg-green-900/30 border-green-600'
|
||||
: data.dynamicATRAnalysis.recommendation.includes('CONSIDER')
|
||||
? 'bg-blue-900/30 border-blue-600'
|
||||
: 'bg-gray-800/50 border-gray-600'
|
||||
: 'bg-yellow-900/30 border-yellow-600'
|
||||
}`}>
|
||||
<div className="font-semibold text-white mb-2">
|
||||
{data.dynamicATRAnalysis.recommendation}
|
||||
</div>
|
||||
<div className="text-sm text-gray-300">
|
||||
{data.dynamicATRAnalysis.reasoning}
|
||||
</div>
|
||||
</div>
|
||||
{/* Loading State */}
|
||||
{loading && analyses.length === 0 && (
|
||||
<div className="text-center py-20">
|
||||
<div className="text-6xl mb-4">⏳</div>
|
||||
<p className="text-xl text-slate-400">Running SQL analyses...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recommendations */}
|
||||
<div className="bg-gradient-to-r from-blue-900/30 to-purple-900/30 border border-blue-700 rounded-xl p-6 mb-8">
|
||||
<h2 className="text-2xl font-bold mb-6 text-white">💡 Optimization Recommendations</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
<RecommendationCard
|
||||
label="Optimal TP1"
|
||||
value={data.recommendations.optimalTP1.toFixed(2) + '%'}
|
||||
current={data.currentLevels.tp1Percent + '%'}
|
||||
color="text-green-400"
|
||||
/>
|
||||
<RecommendationCard
|
||||
label="Optimal TP2"
|
||||
value={data.recommendations.optimalTP2.toFixed(2) + '%'}
|
||||
current={data.currentLevels.tp2Percent + '%'}
|
||||
color="text-blue-400"
|
||||
/>
|
||||
<RecommendationCard
|
||||
label="Optimal SL"
|
||||
value={data.recommendations.optimalSL.toFixed(2) + '%'}
|
||||
current={data.currentLevels.slPercent + '%'}
|
||||
color="text-red-400"
|
||||
/>
|
||||
{/* Analysis Results */}
|
||||
<div className="space-y-6">
|
||||
{analyses.map((analysis, index) => (
|
||||
<AnalysisCard key={index} analysis={analysis} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Reasoning */}
|
||||
<div className="space-y-3 mb-6">
|
||||
<ReasoningCard
|
||||
label="TP1 Reasoning"
|
||||
text={data.recommendations.reasoning.tp1}
|
||||
color="border-green-700 bg-green-900/20"
|
||||
{/* Roadmap Links */}
|
||||
<div className="mt-12 p-6 bg-slate-800/50 rounded-lg border border-slate-700">
|
||||
<h2 className="text-2xl font-bold mb-4">📚 Optimization Roadmaps</h2>
|
||||
<div className="grid md:grid-cols-3 gap-4">
|
||||
<RoadmapLink
|
||||
title="Signal Quality"
|
||||
file="SIGNAL_QUALITY_OPTIMIZATION_ROADMAP.md"
|
||||
status="Phase 1 - Collecting Data"
|
||||
progress="8/20 blocked signals"
|
||||
/>
|
||||
<ReasoningCard
|
||||
label="TP2 Reasoning"
|
||||
text={data.recommendations.reasoning.tp2}
|
||||
color="border-blue-700 bg-blue-900/20"
|
||||
<RoadmapLink
|
||||
title="Position Scaling"
|
||||
file="POSITION_SCALING_ROADMAP.md"
|
||||
status="Phase 1 - Data Collection"
|
||||
progress="Collecting MAE/MFE data"
|
||||
/>
|
||||
<ReasoningCard
|
||||
label="SL Reasoning"
|
||||
text={data.recommendations.reasoning.sl}
|
||||
color="border-red-700 bg-red-900/20"
|
||||
<RoadmapLink
|
||||
title="ATR-Based Targets"
|
||||
file="ATR_BASED_TP_ROADMAP.md"
|
||||
status="Phase 1 - Collecting ATR"
|
||||
progress="Need 50+ trades"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Projected Impact */}
|
||||
<div className="bg-gray-800/50 rounded-lg p-6 border border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">📊 Projected Impact</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<ImpactMetric
|
||||
label="Win Rate Change"
|
||||
value={data.recommendations.projectedImpact.expectedWinRateChange.toFixed(1) + '%'}
|
||||
positive={data.recommendations.projectedImpact.expectedWinRateChange >= 0}
|
||||
/>
|
||||
<ImpactMetric
|
||||
label="Profit Factor Change"
|
||||
value={data.recommendations.projectedImpact.expectedProfitFactorChange.toFixed(2)}
|
||||
positive={data.recommendations.projectedImpact.expectedProfitFactorChange >= 0}
|
||||
/>
|
||||
<ImpactMetric
|
||||
label="Profit Improvement"
|
||||
value={data.recommendations.projectedImpact.estimatedProfitImprovement.toFixed(1) + '%'}
|
||||
positive={data.recommendations.projectedImpact.estimatedProfitImprovement >= 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700 text-center">
|
||||
<p className="text-gray-400 mb-4">
|
||||
Ready to apply these optimized levels? Update your configuration in Settings.
|
||||
</p>
|
||||
<Link
|
||||
href="/settings"
|
||||
className="inline-block px-8 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg font-semibold transition-colors text-white"
|
||||
>
|
||||
⚙️ Go to Settings
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Component helpers
|
||||
function Header() {
|
||||
function AnalysisCard({ analysis }: { analysis: AnalysisResult }) {
|
||||
const statusColors = {
|
||||
loading: 'border-yellow-500 bg-yellow-500/10',
|
||||
success: 'border-green-500 bg-green-500/10',
|
||||
error: 'border-red-500 bg-red-500/10'
|
||||
}
|
||||
|
||||
const statusIcons = {
|
||||
loading: '⏳',
|
||||
success: '✅',
|
||||
error: '❌'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm border-b border-gray-700">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link href="/analytics" className="text-gray-400 hover:text-white transition">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
</Link>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">🎯 TP/SL Optimization</h1>
|
||||
<p className="text-sm text-gray-400">Data-driven recommendations for optimal exit levels</p>
|
||||
<div className={`p-6 rounded-lg border-2 ${statusColors[analysis.status]}`}>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold mb-1 flex items-center gap-2">
|
||||
{statusIcons[analysis.status]} {analysis.name}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400">{analysis.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Display */}
|
||||
{analysis.status === 'success' && analysis.data && (
|
||||
<div className="mt-4">
|
||||
{/* Table */}
|
||||
{Array.isArray(analysis.data) && analysis.data.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-700">
|
||||
{Object.keys(analysis.data[0]).map(key => (
|
||||
<th key={key} className="text-left py-2 px-3 font-medium text-slate-300">
|
||||
{key}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{analysis.data.map((row: any, i: number) => (
|
||||
<tr key={i} className="border-b border-slate-800">
|
||||
{Object.values(row).map((value: any, j: number) => (
|
||||
<td key={j} className="py-2 px-3">
|
||||
{typeof value === 'number' ? value.toFixed(2) : String(value)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Stats */}
|
||||
{typeof analysis.data === 'object' && !Array.isArray(analysis.data) && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{Object.entries(analysis.data).map(([key, value]) => (
|
||||
<div key={key} className="bg-slate-800/50 p-4 rounded">
|
||||
<div className="text-xs text-slate-400 mb-1">{key}</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{typeof value === 'number' ? value.toFixed(2) : String(value)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recommendation */}
|
||||
{analysis.recommendation && (
|
||||
<div className="mt-4 p-4 bg-blue-900/30 border border-blue-500/30 rounded-lg">
|
||||
<div className="text-sm font-medium text-blue-300 mb-1">💡 Recommendation</div>
|
||||
<p className="text-sm">{analysis.recommendation}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action */}
|
||||
{analysis.action && (
|
||||
<div className="mt-4 p-4 bg-purple-900/30 border border-purple-500/30 rounded-lg">
|
||||
<div className="text-sm font-medium text-purple-300 mb-1">🎯 Action Required</div>
|
||||
<p className="text-sm">{analysis.action}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error */}
|
||||
{analysis.status === 'error' && (
|
||||
<div className="mt-4 p-4 bg-red-900/30 border border-red-500/30 rounded-lg">
|
||||
<p className="text-sm text-red-300">{analysis.data?.error || 'Analysis failed'}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatCard({ title, value, valueColor = 'text-white' }: { title: string, value: string, valueColor?: string }) {
|
||||
function RoadmapLink({ title, file, status, progress }: {
|
||||
title: string
|
||||
file: string
|
||||
status: string
|
||||
progress: string
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
|
||||
<div className="text-sm text-gray-400 mb-1">{title}</div>
|
||||
<div className={'text-2xl font-bold ' + valueColor}>{value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricRow({ label, value, valueColor = 'text-white' }: { label: string, value: string, valueColor?: string }) {
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-400">{label}:</span>
|
||||
<span className={'font-semibold ' + valueColor}>{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HitRateBar({ label, hitRate, exits, color }: { label: string, hitRate: number, exits: number, color: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-2">{label}</div>
|
||||
<div className="bg-gray-700 rounded-full h-4 overflow-hidden">
|
||||
<div
|
||||
className={color + ' h-full transition-all duration-500'}
|
||||
style={{ width: hitRate + '%' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1">
|
||||
<div className="text-xs text-gray-400">Hit Rate: {hitRate.toFixed(1)}%</div>
|
||||
<div className="text-xs text-gray-500">{exits} exits</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RecommendationCard({ label, value, current, color }: { label: string, value: string, current: string, color: string }) {
|
||||
return (
|
||||
<div className="bg-gray-800/50 rounded-lg p-4 border border-gray-700">
|
||||
<div className="text-sm text-gray-400 mb-1">{label}</div>
|
||||
<div className={'text-3xl font-bold ' + color}>{value}</div>
|
||||
<div className="text-xs text-gray-400 mt-2">Current: {current}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ReasoningCard({ label, text, color }: { label: string, text: string, color: string }) {
|
||||
return (
|
||||
<div className={'rounded-lg p-4 border ' + color}>
|
||||
<div className="font-semibold text-white mb-1">{label}</div>
|
||||
<div className="text-sm text-gray-300">{text}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ImpactMetric({ label, value, positive }: { label: string, value: string, positive: boolean }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-1">{label}</div>
|
||||
<div className={'text-2xl font-bold ' + (positive ? 'text-green-400' : 'text-red-400')}>
|
||||
{positive ? '+' : ''}{value}
|
||||
</div>
|
||||
<div className="p-4 bg-slate-700/30 rounded-lg border border-slate-600 hover:border-slate-500 transition-colors">
|
||||
<h3 className="font-bold mb-2">{title}</h3>
|
||||
<p className="text-xs text-slate-400 mb-1">{status}</p>
|
||||
<p className="text-xs text-green-400">{progress}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
419
app/api/optimization/analyze/route.ts
Normal file
419
app/api/optimization/analyze/route.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getPrismaClient } from '../../../../lib/database/trades'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET() {
|
||||
const prisma = getPrismaClient()
|
||||
const analyses = []
|
||||
|
||||
try {
|
||||
// ============================================================================
|
||||
// 1. QUALITY SCORE DISTRIBUTION
|
||||
// ============================================================================
|
||||
try {
|
||||
const qualityDistribution = await prisma.$queryRaw<any[]>`
|
||||
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
|
||||
`
|
||||
|
||||
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',
|
||||
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.'
|
||||
})
|
||||
} 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 = await prisma.$queryRaw<any[]>`
|
||||
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 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 = `${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.'
|
||||
}
|
||||
}
|
||||
|
||||
analyses.push({
|
||||
name: 'Direction Performance',
|
||||
description: 'Compare LONG vs SHORT trade outcomes',
|
||||
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 = await prisma.$queryRaw<any[]>`
|
||||
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 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 = `${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.`
|
||||
}
|
||||
|
||||
analyses.push({
|
||||
name: 'Blocked Signals Analysis',
|
||||
description: 'Signals rejected by quality filters',
|
||||
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 = await prisma.$queryRaw<any[]>`
|
||||
SELECT
|
||||
COUNT(*) as total_trades,
|
||||
SUM(CASE WHEN "tp1Hit" = true THEN 1 ELSE 0 END) as tp1_hits,
|
||||
SUM(CASE WHEN "tp2Hit" = true THEN 1 ELSE 0 END) as tp2_hits,
|
||||
ROUND(100.0 * SUM(CASE WHEN "tp1Hit" = true THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as tp1_rate,
|
||||
ROUND(100.0 * SUM(CASE WHEN "tp2Hit" = 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 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)
|
||||
}
|
||||
|
||||
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.`
|
||||
} else if (formattedData.tp2_rate > 50) {
|
||||
recommendation = `TP2 hit rate is ${formattedData.tp2_rate}%. Trailing stop working well - keep current settings.`
|
||||
} else {
|
||||
recommendation = 'Runner performance is within expected range. Continue monitoring.'
|
||||
}
|
||||
|
||||
analyses.push({
|
||||
name: 'Runner Performance',
|
||||
description: 'TP1/TP2 hit rates and max excursion analysis',
|
||||
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 = await prisma.$queryRaw<any[]>`
|
||||
SELECT
|
||||
CASE
|
||||
WHEN atr < 0.3 THEN '<0.3 (Low Vol)'
|
||||
WHEN atr < 0.5 THEN '0.3-0.5 (Med Vol)'
|
||||
WHEN atr < 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 atr IS NOT NULL
|
||||
GROUP BY atr_range
|
||||
ORDER BY MIN(atr)
|
||||
`
|
||||
|
||||
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 = '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.`
|
||||
}
|
||||
|
||||
analyses.push({
|
||||
name: 'ATR vs MFE Correlation',
|
||||
description: 'How volatility affects profit potential',
|
||||
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<any[]>`
|
||||
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 {
|
||||
const dataStatus = await prisma.$queryRaw<any[]>`
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE "exitReason" IS NOT NULL) as completed_trades,
|
||||
COUNT(*) FILTER (WHERE "signalQualityScore" IS NOT NULL) as with_quality,
|
||||
COUNT(*) FILTER (WHERE atr 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"
|
||||
`
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const blockedNeeded = Math.max(0, 20 - formattedData.blocked_quality)
|
||||
const v8Needed = Math.max(0, 50 - formattedData.v8_trades)
|
||||
|
||||
let action = ''
|
||||
if (blockedNeeded > 0) {
|
||||
action = `Need ${blockedNeeded} more blocked signals for Phase 2 quality threshold analysis.`
|
||||
}
|
||||
if (v8Needed > 0) {
|
||||
if (action) action += ' '
|
||||
action += `Need ${v8Needed} more v8 indicator trades for statistical validation.`
|
||||
}
|
||||
|
||||
analyses.push({
|
||||
name: 'Data Collection Status',
|
||||
description: 'Progress toward analysis milestones',
|
||||
status: 'success',
|
||||
data: formattedData,
|
||||
recommendation: action || '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,
|
||||
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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user