Merge pull request #9 from mindesbunister/copilot/fix-analytics-dashboard-filtering
Add indicator version filtering to analytics optimization dashboard
This commit is contained in:
@@ -11,15 +11,25 @@ interface AnalysisResult {
|
|||||||
action?: string
|
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() {
|
export default function OptimizationPage() {
|
||||||
const [analyses, setAnalyses] = useState<AnalysisResult[]>([])
|
const [analyses, setAnalyses] = useState<AnalysisResult[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [lastRefresh, setLastRefresh] = useState<Date | null>(null)
|
const [lastRefresh, setLastRefresh] = useState<Date | null>(null)
|
||||||
|
const [selectedVersion, setSelectedVersion] = useState<string>('v9') // Default to current production
|
||||||
|
|
||||||
const loadAnalyses = async () => {
|
const loadAnalyses = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/optimization/analyze')
|
const response = await fetch(`/api/optimization/analyze?version=${encodeURIComponent(selectedVersion)}`)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setAnalyses(data.analyses)
|
setAnalyses(data.analyses)
|
||||||
setLastRefresh(new Date())
|
setLastRefresh(new Date())
|
||||||
@@ -32,7 +42,8 @@ export default function OptimizationPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAnalyses()
|
loadAnalyses()
|
||||||
}, [])
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedVersion]) // Reload when version changes
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white p-8">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white p-8">
|
||||||
@@ -60,8 +71,25 @@ export default function OptimizationPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Refresh Button */}
|
{/* Version Selector and Refresh Button */}
|
||||||
<div className="mb-6">
|
<div className="mb-6 flex items-center gap-4 flex-wrap">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<label htmlFor="version-select" className="text-sm font-medium text-slate-300">
|
||||||
|
Indicator Version:
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="version-select"
|
||||||
|
value={selectedVersion}
|
||||||
|
onChange={(e) => setSelectedVersion(e.target.value)}
|
||||||
|
className="px-4 py-2 bg-slate-700 rounded-lg border border-slate-600 text-white focus:border-blue-500 focus:outline-none"
|
||||||
|
>
|
||||||
|
{INDICATOR_VERSIONS.map((v) => (
|
||||||
|
<option key={v.value} value={v.value}>
|
||||||
|
{v.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={loadAnalyses}
|
onClick={loadAnalyses}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -69,6 +97,11 @@ export default function OptimizationPage() {
|
|||||||
>
|
>
|
||||||
{loading ? '🔄 Analyzing...' : '🔄 Refresh All Analyses'}
|
{loading ? '🔄 Analyzing...' : '🔄 Refresh All Analyses'}
|
||||||
</button>
|
</button>
|
||||||
|
{selectedVersion !== 'all' && (
|
||||||
|
<span className="text-sm text-green-400 font-medium">
|
||||||
|
✓ Filtering by {selectedVersion.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Loading State */}
|
{/* Loading State */}
|
||||||
|
|||||||
@@ -1,18 +1,38 @@
|
|||||||
import { NextResponse } from 'next/server'
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { getPrismaClient } from '../../../../lib/database/trades'
|
import { getPrismaClient } from '../../../../lib/database/trades'
|
||||||
|
import { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
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 prisma = getPrismaClient()
|
||||||
const analyses = []
|
const analyses = []
|
||||||
|
|
||||||
|
// Version label for recommendations
|
||||||
|
const versionLabel = version === 'all' ? 'All versions combined' : `${version.toUpperCase()} only`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 1. QUALITY SCORE DISTRIBUTION
|
// 1. QUALITY SCORE DISTRIBUTION
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
try {
|
try {
|
||||||
const qualityDistribution = await prisma.$queryRaw<any[]>`
|
// Build query with optional version filter
|
||||||
|
const qualityDistribution = version === 'all'
|
||||||
|
? await prisma.$queryRaw<any[]>`
|
||||||
SELECT
|
SELECT
|
||||||
CASE
|
CASE
|
||||||
WHEN "signalQualityScore" >= 95 THEN '95-100 (Excellent)'
|
WHEN "signalQualityScore" >= 95 THEN '95-100 (Excellent)'
|
||||||
@@ -36,6 +56,31 @@ export async function GET() {
|
|||||||
GROUP BY tier
|
GROUP BY tier
|
||||||
ORDER BY MIN("signalQualityScore") DESC
|
ORDER BY MIN("signalQualityScore") DESC
|
||||||
`
|
`
|
||||||
|
: 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
|
||||||
|
AND "indicatorVersion" = ${version}
|
||||||
|
GROUP BY tier
|
||||||
|
ORDER BY MIN("signalQualityScore") DESC
|
||||||
|
`
|
||||||
|
|
||||||
const formattedData = qualityDistribution.map(row => ({
|
const formattedData = qualityDistribution.map(row => ({
|
||||||
tier: row.tier,
|
tier: row.tier,
|
||||||
@@ -53,12 +98,12 @@ export async function GET() {
|
|||||||
|
|
||||||
analyses.push({
|
analyses.push({
|
||||||
name: 'Quality Score Distribution',
|
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',
|
status: 'success',
|
||||||
data: formattedData,
|
data: formattedData,
|
||||||
recommendation: bestTier.win_rate >= 70
|
recommendation: bestTier.win_rate >= 70
|
||||||
? `Quality ${bestTier.tier.split(' ')[0]} shows ${bestTier.win_rate}% WR. Consider raising threshold to this tier.`
|
? `${versionLabel}: 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}: Continue collecting data for reliable quality threshold optimization.`
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
analyses.push({
|
analyses.push({
|
||||||
@@ -73,7 +118,8 @@ export async function GET() {
|
|||||||
// 2. DIRECTION PERFORMANCE (Long vs Short)
|
// 2. DIRECTION PERFORMANCE (Long vs Short)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
try {
|
try {
|
||||||
const directionPerformance = await prisma.$queryRaw<any[]>`
|
const directionPerformance = version === 'all'
|
||||||
|
? await prisma.$queryRaw<any[]>`
|
||||||
SELECT
|
SELECT
|
||||||
direction,
|
direction,
|
||||||
COUNT(*) as trades,
|
COUNT(*) as trades,
|
||||||
@@ -88,6 +134,22 @@ export async function GET() {
|
|||||||
GROUP BY direction
|
GROUP BY direction
|
||||||
ORDER BY win_rate DESC
|
ORDER BY win_rate DESC
|
||||||
`
|
`
|
||||||
|
: 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
|
||||||
|
AND "indicatorVersion" = ${version}
|
||||||
|
GROUP BY direction
|
||||||
|
ORDER BY win_rate DESC
|
||||||
|
`
|
||||||
|
|
||||||
const formattedData = directionPerformance.map(row => ({
|
const formattedData = directionPerformance.map(row => ({
|
||||||
direction: String(row.direction).toUpperCase(),
|
direction: String(row.direction).toUpperCase(),
|
||||||
@@ -108,15 +170,15 @@ export async function GET() {
|
|||||||
if (wrDiff > 15) {
|
if (wrDiff > 15) {
|
||||||
const better = longData.win_rate > shortData.win_rate ? 'LONG' : 'SHORT'
|
const better = longData.win_rate > shortData.win_rate ? 'LONG' : 'SHORT'
|
||||||
const worse = better === 'LONG' ? 'SHORT' : 'LONG'
|
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 {
|
} else {
|
||||||
recommendation = 'Direction performance is balanced. No threshold adjustment needed.'
|
recommendation = `${versionLabel}: Direction performance is balanced. No threshold adjustment needed.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
analyses.push({
|
analyses.push({
|
||||||
name: 'Direction Performance',
|
name: 'Direction Performance',
|
||||||
description: 'Compare LONG vs SHORT trade outcomes',
|
description: `Compare LONG vs SHORT trade outcomes (${versionLabel})`,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: formattedData,
|
data: formattedData,
|
||||||
recommendation
|
recommendation
|
||||||
@@ -134,7 +196,8 @@ export async function GET() {
|
|||||||
// 3. BLOCKED SIGNALS ANALYSIS
|
// 3. BLOCKED SIGNALS ANALYSIS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
try {
|
try {
|
||||||
const blockedSignals = await prisma.$queryRaw<any[]>`
|
const blockedSignals = version === 'all'
|
||||||
|
? await prisma.$queryRaw<any[]>`
|
||||||
SELECT
|
SELECT
|
||||||
"blockReason",
|
"blockReason",
|
||||||
COUNT(*) as count,
|
COUNT(*) as count,
|
||||||
@@ -146,6 +209,19 @@ export async function GET() {
|
|||||||
GROUP BY "blockReason"
|
GROUP BY "blockReason"
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
`
|
`
|
||||||
|
: 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
|
||||||
|
AND "indicatorVersion" = ${version}
|
||||||
|
GROUP BY "blockReason"
|
||||||
|
ORDER BY count DESC
|
||||||
|
`
|
||||||
|
|
||||||
const formattedData = blockedSignals.map(row => ({
|
const formattedData = blockedSignals.map(row => ({
|
||||||
reason: String(row.blockReason),
|
reason: String(row.blockReason),
|
||||||
@@ -158,15 +234,15 @@ export async function GET() {
|
|||||||
const qualityBlocked = formattedData.find(d => d.reason === 'QUALITY_SCORE_TOO_LOW')
|
const qualityBlocked = formattedData.find(d => d.reason === 'QUALITY_SCORE_TOO_LOW')
|
||||||
let recommendation = ''
|
let recommendation = ''
|
||||||
if (qualityBlocked && qualityBlocked.count >= 20) {
|
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 {
|
} else {
|
||||||
const needed = qualityBlocked ? 20 - qualityBlocked.count : 20
|
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({
|
analyses.push({
|
||||||
name: 'Blocked Signals Analysis',
|
name: 'Blocked Signals Analysis',
|
||||||
description: 'Signals rejected by quality filters',
|
description: `Signals rejected by quality filters (${versionLabel})`,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: formattedData,
|
data: formattedData,
|
||||||
recommendation,
|
recommendation,
|
||||||
@@ -187,43 +263,60 @@ export async function GET() {
|
|||||||
// 4. RUNNER PERFORMANCE ANALYSIS
|
// 4. RUNNER PERFORMANCE ANALYSIS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
try {
|
try {
|
||||||
const runnerPerformance = await prisma.$queryRaw<any[]>`
|
const runnerPerformance = version === 'all'
|
||||||
|
? await prisma.$queryRaw<any[]>`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_trades,
|
COUNT(*) as total_trades,
|
||||||
SUM(CASE WHEN "tp1Filled" = true THEN 1 ELSE 0 END) as tp1_hits,
|
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,
|
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 "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) / COUNT(*)::numeric, 1) as tp2_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("maxFavorableExcursion")::numeric, 2) as avg_mfe,
|
||||||
ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_mae
|
ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_mae
|
||||||
FROM "Trade"
|
FROM "Trade"
|
||||||
WHERE "exitReason" IS NOT NULL
|
WHERE "exitReason" IS NOT NULL
|
||||||
AND "createdAt" >= NOW() - INTERVAL '30 days'
|
AND "createdAt" >= NOW() - INTERVAL '30 days'
|
||||||
`
|
`
|
||||||
|
: await prisma.$queryRaw<any[]>`
|
||||||
|
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 data = runnerPerformance[0]
|
||||||
const formattedData = {
|
const formattedData = {
|
||||||
total_trades: Number(data.total_trades),
|
total_trades: Number(data.total_trades) || 0,
|
||||||
tp1_hits: Number(data.tp1_hits),
|
tp1_hits: Number(data.tp1_hits) || 0,
|
||||||
tp2_hits: Number(data.tp2_hits),
|
tp2_hits: Number(data.tp2_hits) || 0,
|
||||||
tp1_rate: Number(data.tp1_rate),
|
tp1_rate: Number(data.tp1_rate) || 0,
|
||||||
tp2_rate: Number(data.tp2_rate),
|
tp2_rate: Number(data.tp2_rate) || 0,
|
||||||
avg_mfe: Number(data.avg_mfe),
|
avg_mfe: Number(data.avg_mfe) || 0,
|
||||||
avg_mae: Number(data.avg_mae)
|
avg_mae: Number(data.avg_mae) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let recommendation = ''
|
let recommendation = ''
|
||||||
if (formattedData.avg_mfe > formattedData.tp1_rate * 1.5) {
|
if (formattedData.total_trades === 0) {
|
||||||
recommendation = `Avg MFE (${formattedData.avg_mfe.toFixed(2)}%) significantly exceeds TP1 rate. Consider widening TP1 or increasing runner size.`
|
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) {
|
} 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 {
|
} else {
|
||||||
recommendation = 'Runner performance is within expected range. Continue monitoring.'
|
recommendation = `${versionLabel}: Runner performance is within expected range. Continue monitoring.`
|
||||||
}
|
}
|
||||||
|
|
||||||
analyses.push({
|
analyses.push({
|
||||||
name: 'Runner Performance',
|
name: 'Runner Performance',
|
||||||
description: 'TP1/TP2 hit rates and max excursion analysis',
|
description: `TP1/TP2 hit rates and max excursion analysis (${versionLabel})`,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: formattedData,
|
data: formattedData,
|
||||||
recommendation
|
recommendation
|
||||||
@@ -241,7 +334,8 @@ export async function GET() {
|
|||||||
// 5. ATR CORRELATION WITH MFE
|
// 5. ATR CORRELATION WITH MFE
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
try {
|
try {
|
||||||
const atrCorrelation = await prisma.$queryRaw<any[]>`
|
const atrCorrelation = version === 'all'
|
||||||
|
? await prisma.$queryRaw<any[]>`
|
||||||
SELECT
|
SELECT
|
||||||
CASE
|
CASE
|
||||||
WHEN "atrAtEntry" < 0.3 THEN '<0.3 (Low Vol)'
|
WHEN "atrAtEntry" < 0.3 THEN '<0.3 (Low Vol)'
|
||||||
@@ -259,6 +353,25 @@ export async function GET() {
|
|||||||
GROUP BY atr_range
|
GROUP BY atr_range
|
||||||
ORDER BY MIN("atrAtEntry")
|
ORDER BY MIN("atrAtEntry")
|
||||||
`
|
`
|
||||||
|
: await prisma.$queryRaw<any[]>`
|
||||||
|
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 => ({
|
const formattedData = atrCorrelation.map(row => ({
|
||||||
atr_range: String(row.atr_range),
|
atr_range: String(row.atr_range),
|
||||||
@@ -268,17 +381,19 @@ export async function GET() {
|
|||||||
win_rate: Number(row.win_rate)
|
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) {
|
if (formattedData.length >= 3) {
|
||||||
const highestMFE = formattedData.reduce((best, current) =>
|
const highestMFE = formattedData.reduce((best, current) =>
|
||||||
current.avg_mfe > best.avg_mfe ? current : best
|
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({
|
analyses.push({
|
||||||
name: 'ATR vs MFE Correlation',
|
name: 'ATR vs MFE Correlation',
|
||||||
description: 'How volatility affects profit potential',
|
description: `How volatility affects profit potential (${versionLabel})`,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: formattedData,
|
data: formattedData,
|
||||||
recommendation
|
recommendation
|
||||||
@@ -352,45 +467,59 @@ export async function GET() {
|
|||||||
// 7. DATA COLLECTION STATUS
|
// 7. DATA COLLECTION STATUS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
try {
|
try {
|
||||||
const dataStatus = await prisma.$queryRaw<any[]>`
|
// For 'all' version, show overall data collection status
|
||||||
|
// For specific version, show that version's data status
|
||||||
|
const dataStatus = version === 'all'
|
||||||
|
? await prisma.$queryRaw<any[]>`
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) FILTER (WHERE "exitReason" IS NOT NULL) as completed_trades,
|
COUNT(*) FILTER (WHERE "exitReason" IS NOT NULL) as completed_trades,
|
||||||
COUNT(*) FILTER (WHERE "signalQualityScore" IS NOT NULL) as with_quality,
|
COUNT(*) FILTER (WHERE "signalQualityScore" IS NOT NULL) as with_quality,
|
||||||
COUNT(*) FILTER (WHERE "atrAtEntry" IS NOT NULL) as with_atr,
|
COUNT(*) FILTER (WHERE "atrAtEntry" IS NOT NULL) as with_atr,
|
||||||
COUNT(*) FILTER (WHERE "maxFavorableExcursion" IS NOT NULL) as with_mfe,
|
COUNT(*) FILTER (WHERE "maxFavorableExcursion" IS NOT NULL) as with_mfe,
|
||||||
COUNT(*) FILTER (WHERE "indicatorVersion" = 'v8') as v8_trades,
|
COUNT(*) FILTER (WHERE "indicatorVersion" = 'v9') as version_trades,
|
||||||
(SELECT COUNT(*) FROM "BlockedSignal" WHERE "blockReason" = 'QUALITY_SCORE_TOO_LOW') as blocked_quality
|
(SELECT COUNT(*) FROM "BlockedSignal" WHERE "blockReason" = 'QUALITY_SCORE_TOO_LOW') as blocked_quality
|
||||||
FROM "Trade"
|
FROM "Trade"
|
||||||
`
|
`
|
||||||
|
: await prisma.$queryRaw<any[]>`
|
||||||
|
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 data = dataStatus[0]
|
||||||
const formattedData = {
|
const formattedData = {
|
||||||
completed_trades: Number(data.completed_trades),
|
completed_trades: Number(data.completed_trades) || 0,
|
||||||
with_quality: Number(data.with_quality),
|
with_quality: Number(data.with_quality) || 0,
|
||||||
with_atr: Number(data.with_atr),
|
with_atr: Number(data.with_atr) || 0,
|
||||||
with_mfe: Number(data.with_mfe),
|
with_mfe: Number(data.with_mfe) || 0,
|
||||||
v8_trades: Number(data.v8_trades),
|
version_trades: Number(data.version_trades) || 0,
|
||||||
blocked_quality: Number(data.blocked_quality)
|
blocked_quality: Number(data.blocked_quality) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockedNeeded = Math.max(0, 20 - formattedData.blocked_quality)
|
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 = ''
|
let action = ''
|
||||||
if (blockedNeeded > 0) {
|
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 += ' '
|
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({
|
analyses.push({
|
||||||
name: 'Data Collection Status',
|
name: 'Data Collection Status',
|
||||||
description: 'Progress toward analysis milestones',
|
description: `Progress toward analysis milestones (${versionLabel})`,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: formattedData,
|
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
|
action: action || undefined
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -405,6 +534,7 @@ export async function GET() {
|
|||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
analyses,
|
analyses,
|
||||||
|
selectedVersion: version,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
1
package-lock.json
generated
1
package-lock.json
generated
@@ -12951,7 +12951,6 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|||||||
Reference in New Issue
Block a user