diff --git a/app/analytics/page.tsx b/app/analytics/page.tsx index 8819635..589cebd 100644 --- a/app/analytics/page.tsx +++ b/app/analytics/page.tsx @@ -67,10 +67,34 @@ interface PositionSummary { netPositions: NetPosition[] } +interface VersionStats { + version: string + tradeCount: number + winRate: number + totalPnL: number + avgPnL: number + avgQualityScore: number | null + avgMFE: number | null + avgMAE: number | null + extremePositions: { + count: number + avgADX: number | null + weakADXCount: number + winRate: number + avgPnL: number + } +} + +interface VersionComparison { + versions: VersionStats[] + descriptions: Record +} + export default function AnalyticsPage() { const [stats, setStats] = useState(null) const [positions, setPositions] = useState(null) const [lastTrade, setLastTrade] = useState(null) + const [versionComparison, setVersionComparison] = useState(null) const [loading, setLoading] = useState(true) const [selectedDays, setSelectedDays] = useState(30) @@ -81,19 +105,22 @@ export default function AnalyticsPage() { const loadData = async () => { setLoading(true) try { - const [statsRes, positionsRes, lastTradeRes] = await Promise.all([ + const [statsRes, positionsRes, lastTradeRes, versionRes] = await Promise.all([ fetch(`/api/analytics/stats?days=${selectedDays}`), fetch('/api/analytics/positions'), fetch('/api/analytics/last-trade'), + fetch('/api/analytics/version-comparison'), ]) const statsData = await statsRes.json() const positionsData = await positionsRes.json() const lastTradeData = await lastTradeRes.json() + const versionData = await versionRes.json() setStats(statsData.stats) setPositions(positionsData.summary) setLastTrade(lastTradeData.trade) + setVersionComparison(versionData.success ? versionData : null) } catch (error) { console.error('Failed to load analytics:', error) } @@ -250,6 +277,180 @@ export default function AnalyticsPage() { )} + {/* Signal Quality Version Comparison */} + {versionComparison && versionComparison.versions.length > 0 && ( +
+

🔬 Signal Quality Logic Versions

+
+

+ The bot has evolved through different signal quality scoring algorithms. + This section compares their performance to enable data-driven optimization. +

+ +
+ {versionComparison.versions.map((version, idx) => { + const isCurrentVersion = version.version === 'v3' + return ( +
+
+
+
+

+ {version.version.toUpperCase()} + {isCurrentVersion && ( + + CURRENT + + )} +

+
+

+ {versionComparison.descriptions[version.version] || 'Unknown version'} +

+
+
+ + {/* Main Metrics Grid */} +
+
+
Trades
+
{version.tradeCount}
+
+ +
+
Win Rate
+
= 50 ? 'text-green-400' : 'text-red-400'}`}> + {version.winRate}% +
+
+ +
+
Total P&L
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {version.totalPnL >= 0 ? '+' : ''}${version.totalPnL.toFixed(2)} +
+
+ +
+
Avg P&L
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {version.avgPnL >= 0 ? '+' : ''}${version.avgPnL.toFixed(2)} +
+
+
+ + {/* Advanced Metrics */} +
+ {version.avgQualityScore !== null && ( +
+
Avg Quality Score
+
= 75 ? 'text-green-400' : 'text-yellow-400'}`}> + {version.avgQualityScore}/100 +
+
+ )} + + {version.avgMFE !== null && ( +
+
Avg MFE
+
+ +{version.avgMFE.toFixed(2)}% +
+
+ )} + + {version.avgMAE !== null && ( +
+
Avg MAE
+
+ {version.avgMAE.toFixed(2)}% +
+
+ )} +
+ + {/* Extreme Position Stats */} + {version.extremePositions.count > 0 && ( +
+
+ ⚠️ + Extreme Positions (< 15% or > 85% range) +
+
+
+
Count
+
+ {version.extremePositions.count} +
+
+ + {version.extremePositions.avgADX !== null && ( +
+
Avg ADX
+
= 18 ? 'text-green-400' : 'text-orange-400'}`}> + {version.extremePositions.avgADX.toFixed(1)} +
+
+ )} + +
+
Weak ADX
+
+ {version.extremePositions.weakADXCount} +
+
+ +
+
Win Rate
+
= 50 ? 'text-green-400' : 'text-red-400'}`}> + {version.extremePositions.winRate}% +
+
+ +
+
Avg P&L
+
= 0 ? 'text-green-400' : 'text-red-400'}`}> + {version.extremePositions.avgPnL >= 0 ? '+' : ''}${version.extremePositions.avgPnL.toFixed(2)} +
+
+
+
+ )} + + {/* Data Collection Notice for v3 */} + {isCurrentVersion && version.tradeCount < 20 && ( +
+
+ 📊 +

+ Data Collection Phase: Need {20 - version.tradeCount} more trades + before v3 performance can be reliably evaluated. This version is designed to prevent + losses from extreme position entries with weak trends (ADX < 18). +

+
+
+ )} +
+ ) + })} +
+ + {/* Legend */} +
+
+
MFE (Max Favorable Excursion): Best profit % reached during trade lifetime
+
MAE (Max Adverse Excursion): Worst loss % reached during trade lifetime
+
Extreme Positions: Trades entered at price range extremes (< 15% or > 85%)
+
Weak ADX: Trend strength below 18 (indicates sideways/choppy market)
+
+
+
+
+ )} + {/* Last Trade Details */} {lastTrade && (
diff --git a/app/api/analytics/version-comparison/route.ts b/app/api/analytics/version-comparison/route.ts new file mode 100644 index 0000000..a5875c7 --- /dev/null +++ b/app/api/analytics/version-comparison/route.ts @@ -0,0 +1,139 @@ +/** + * Trading Bot v4 - Signal Quality Version Comparison API + * + * Returns performance metrics comparing different signal quality scoring versions + */ + +import { NextResponse } from 'next/server' +import { getPrismaClient } from '@/lib/database/trades' + +export const dynamic = 'force-dynamic' + +interface VersionStats { + version: string + tradeCount: number + winRate: number + totalPnL: number + avgPnL: number + avgQualityScore: number | null + avgMFE: number | null + avgMAE: number | null + extremePositions: { + count: number + avgADX: number | null + weakADXCount: number + winRate: number + avgPnL: number + } +} + +export async function GET() { + try { + const prisma = getPrismaClient() + + // Get overall stats by version + const versionStats = await prisma.$queryRaw>` + SELECT + COALESCE("signalQualityVersion", 'v1') as version, + COUNT(*) as trades, + SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) as wins, + ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl, + ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, + ROUND(AVG("signalQualityScore")::numeric, 1) as avg_quality_score, + ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, + ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_mae + FROM "Trade" + WHERE "exitReason" IS NOT NULL + AND "exitReason" NOT LIKE '%CLEANUP%' + AND "isTestTrade" = false + GROUP BY "signalQualityVersion" + ORDER BY version DESC + ` + + // Get extreme position stats by version (< 15% or > 85%) + const extremePositionStats = await prisma.$queryRaw>` + SELECT + COALESCE("signalQualityVersion", 'v1') as version, + COUNT(*) as count, + ROUND(AVG("adxAtEntry")::numeric, 1) as avg_adx, + COUNT(*) FILTER (WHERE "adxAtEntry" < 18) as weak_adx_count, + SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) as wins, + ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl + FROM "Trade" + WHERE "exitReason" IS NOT NULL + AND "exitReason" NOT LIKE '%CLEANUP%' + AND "isTestTrade" = false + AND "pricePositionAtEntry" IS NOT NULL + AND ("pricePositionAtEntry" < 15 OR "pricePositionAtEntry" > 85) + GROUP BY "signalQualityVersion" + ORDER BY version DESC + ` + + // Build combined results + const results: VersionStats[] = versionStats.map(stat => { + const extremeStats = extremePositionStats.find(e => + (e.version || 'v1') === (stat.version || 'v1') + ) + + const trades = Number(stat.trades) + const wins = Number(stat.wins) + const extremeCount = extremeStats ? Number(extremeStats.count) : 0 + const extremeWins = extremeStats ? Number(extremeStats.wins) : 0 + + return { + version: stat.version || 'v1', + tradeCount: trades, + winRate: trades > 0 ? Math.round((wins / trades) * 100 * 10) / 10 : 0, + totalPnL: stat.total_pnl, + avgPnL: stat.avg_pnl, + avgQualityScore: stat.avg_quality_score, + avgMFE: stat.avg_mfe, + avgMAE: stat.avg_mae, + extremePositions: { + count: extremeCount, + avgADX: extremeStats?.avg_adx || null, + weakADXCount: extremeStats ? Number(extremeStats.weak_adx_count) : 0, + winRate: extremeCount > 0 ? Math.round((extremeWins / extremeCount) * 100 * 10) / 10 : 0, + avgPnL: extremeStats?.avg_pnl || 0, + } + } + }) + + // Get version descriptions + const versionDescriptions: Record = { + 'v1': 'Original logic (price < 5% threshold)', + 'v2': 'Added volume compensation for low ADX', + 'v3': 'Stricter: ADX > 18 required for positions < 15%' + } + + return NextResponse.json({ + success: true, + versions: results, + descriptions: versionDescriptions, + timestamp: new Date().toISOString() + }) + + } catch (error) { + console.error('❌ Failed to fetch version comparison:', error) + return NextResponse.json( + { success: false, error: 'Failed to fetch version comparison data' }, + { status: 500 } + ) + } +} diff --git a/docs/analysis/SIGNAL_QUALITY_VERSION_ANALYSIS.sql b/docs/analysis/SIGNAL_QUALITY_VERSION_ANALYSIS.sql new file mode 100644 index 0000000..02bb96b --- /dev/null +++ b/docs/analysis/SIGNAL_QUALITY_VERSION_ANALYSIS.sql @@ -0,0 +1,124 @@ +-- Signal Quality Version Analysis +-- Compare performance between different scoring logic versions + +-- 1. Count trades by version +SELECT + COALESCE("signalQualityVersion", 'v1/null') as version, + COUNT(*) as trade_count, + ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER(), 1) as percentage +FROM "Trade" +WHERE "exitReason" IS NOT NULL +GROUP BY "signalQualityVersion" +ORDER BY version; + +-- 2. Performance by version +SELECT + COALESCE("signalQualityVersion", 'v1/null') as version, + COUNT(*) as trades, + ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, + ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl, + ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate, + ROUND(AVG("signalQualityScore")::numeric, 1) as avg_quality_score, + ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, + ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_mae +FROM "Trade" +WHERE "exitReason" IS NOT NULL AND "exitReason" NOT LIKE '%CLEANUP%' +GROUP BY "signalQualityVersion" +ORDER BY version; + +-- 3. Version breakdown by exit reason +SELECT + COALESCE("signalQualityVersion", 'v1/null') as version, + "exitReason", + COUNT(*) as count, + ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl +FROM "Trade" +WHERE "exitReason" IS NOT NULL AND "exitReason" NOT LIKE '%CLEANUP%' +GROUP BY "signalQualityVersion", "exitReason" +ORDER BY version, count DESC; + +-- 4. Quality score distribution by version +SELECT + COALESCE("signalQualityVersion", 'v1/null') as version, + CASE + WHEN "signalQualityScore" >= 80 THEN '80-100 (High)' + WHEN "signalQualityScore" >= 70 THEN '70-79 (Good)' + WHEN "signalQualityScore" >= 60 THEN '60-69 (Pass)' + ELSE '< 60 (Block)' + END as score_range, + COUNT(*) as trades, + 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 "signalQualityScore" IS NOT NULL +GROUP BY "signalQualityVersion", score_range +ORDER BY version, score_range DESC; + +-- 5. Extreme position entries by version (< 15% or > 85%) +SELECT + COALESCE("signalQualityVersion", 'v1/null') as version, + direction, + COUNT(*) as trades, + ROUND(AVG("pricePositionAtEntry")::numeric, 1) as avg_price_pos, + ROUND(AVG("adxAtEntry")::numeric, 1) as avg_adx, + 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 "pricePositionAtEntry" IS NOT NULL + AND ("pricePositionAtEntry" < 15 OR "pricePositionAtEntry" > 85) +GROUP BY "signalQualityVersion", direction +ORDER BY version, direction; + +-- 6. Recent v3 trades (new logic) +SELECT + "createdAt", + symbol, + direction, + "entryPrice", + "exitPrice", + "exitReason", + ROUND("realizedPnL"::numeric, 2) as pnl, + "signalQualityScore" as score, + ROUND("adxAtEntry"::numeric, 1) as adx, + ROUND("pricePositionAtEntry"::numeric, 1) as price_pos +FROM "Trade" +WHERE "signalQualityVersion" = 'v3' + AND "exitReason" IS NOT NULL +ORDER BY "createdAt" DESC +LIMIT 20; + +-- 7. Compare v3 vs pre-v3 on extreme positions +WITH version_groups AS ( + SELECT + CASE WHEN "signalQualityVersion" = 'v3' THEN 'v3 (NEW)' ELSE 'pre-v3 (OLD)' END as version_group, + * + FROM "Trade" + WHERE "exitReason" IS NOT NULL + AND "pricePositionAtEntry" IS NOT NULL + AND ("pricePositionAtEntry" < 15 OR "pricePositionAtEntry" > 85) + AND "adxAtEntry" IS NOT NULL +) +SELECT + version_group, + COUNT(*) as trades, + ROUND(AVG("adxAtEntry")::numeric, 1) as avg_adx, + COUNT(*) FILTER (WHERE "adxAtEntry" < 18) as weak_adx_count, + ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, + ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl, + ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate +FROM version_groups +GROUP BY version_group +ORDER BY version_group DESC; + +-- 8. Daily performance by version (last 7 days) +SELECT + DATE("createdAt") as trade_date, + COALESCE("signalQualityVersion", 'v1/null') as version, + COUNT(*) as trades, + ROUND(SUM("realizedPnL")::numeric, 2) as daily_pnl +FROM "Trade" +WHERE "exitReason" IS NOT NULL + AND "createdAt" >= NOW() - INTERVAL '7 days' +GROUP BY trade_date, "signalQualityVersion" +ORDER BY trade_date DESC, version;