diff --git a/app/api/analytics/tp-sl-optimization/route.ts b/app/api/analytics/tp-sl-optimization/route.ts new file mode 100644 index 0000000..0697c2c --- /dev/null +++ b/app/api/analytics/tp-sl-optimization/route.ts @@ -0,0 +1,319 @@ +/** + * TP/SL Optimization API Endpoint + * + * Analyzes historical trades using MAE/MFE data to recommend optimal TP/SL levels + * GET /api/analytics/tp-sl-optimization + */ + +import { NextRequest, NextResponse } from 'next/server' +import { getPrismaClient } from '@/lib/database/trades' + +export interface TPSLOptimizationResponse { + success: boolean + analysis?: { + totalTrades: number + winningTrades: number + losingTrades: number + winRate: number + avgWin: number + avgLoss: number + profitFactor: number + + // MAE/MFE Analysis + maeAnalysis: { + avgMAE: number + medianMAE: number + percentile25MAE: number + percentile75MAE: number + worstMAE: number + } + + mfeAnalysis: { + avgMFE: number + medianMFE: number + percentile25MFE: number + percentile75MFE: number + bestMFE: number + } + + // Current Configuration Performance + currentLevels: { + tp1Percent: number + tp2Percent: number + slPercent: number + tp1HitRate: number + tp2HitRate: number + slHitRate: number + moneyLeftOnTable: number // Sum of (MFE - realized P&L) for winning trades + } + + // Recommendations + recommendations: { + optimalTP1: number // 50% of avg MFE + optimalTP2: number // 80% of avg MFE + optimalSL: number // 70% of avg MAE (tighter to catch losers early) + + reasoning: { + tp1: string + tp2: string + sl: string + } + + projectedImpact: { + expectedWinRateChange: number + expectedProfitFactorChange: number + estimatedProfitImprovement: number // % improvement in total P&L + } + } + + // Detailed Trade Stats + tradesByOutcome: { + tp1Exits: number + tp2Exits: number + slExits: number + manualExits: number + } + } + error?: string +} + +export async function GET(request: NextRequest): Promise> { + try { + const prisma = getPrismaClient() + + // Get all closed trades with MAE/MFE data + const trades = await prisma.trade.findMany({ + where: { + status: 'closed', + maxFavorableExcursion: { not: null }, + maxAdverseExcursion: { not: null }, + }, + orderBy: { + entryTime: 'desc', + }, + }) + + if (trades.length < 10) { + return NextResponse.json({ + success: false, + error: `Insufficient data: Only ${trades.length} trades found. Need at least 10 trades with MAE/MFE data for meaningful analysis.`, + }) + } + + console.log(`📊 Analyzing ${trades.length} trades for TP/SL optimization`) + + // Separate winning and losing trades + const winningTrades = trades.filter(t => (t.realizedPnL || 0) > 0) + const losingTrades = trades.filter(t => (t.realizedPnL || 0) <= 0) + + // Calculate basic stats + const totalPnL = trades.reduce((sum, t) => sum + (t.realizedPnL || 0), 0) + const avgWin = winningTrades.length > 0 + ? winningTrades.reduce((sum, t) => sum + (t.realizedPnL || 0), 0) / winningTrades.length + : 0 + const avgLoss = losingTrades.length > 0 + ? Math.abs(losingTrades.reduce((sum, t) => sum + (t.realizedPnL || 0), 0) / losingTrades.length) + : 0 + const winRate = (winningTrades.length / trades.length) * 100 + const profitFactor = avgLoss > 0 ? (avgWin * winningTrades.length) / (avgLoss * losingTrades.length) : 0 + + // MAE Analysis (how far price moved against us) + const maeValues = trades + .map(t => t.maxAdverseExcursion!) + .filter(v => v !== null && v !== undefined) + .sort((a, b) => a - b) + + const avgMAE = maeValues.reduce((sum, v) => sum + v, 0) / maeValues.length + const medianMAE = maeValues[Math.floor(maeValues.length / 2)] + const percentile25MAE = maeValues[Math.floor(maeValues.length * 0.25)] + const percentile75MAE = maeValues[Math.floor(maeValues.length * 0.75)] + const worstMAE = Math.min(...maeValues) + + // MFE Analysis (how far price moved in our favor) + const mfeValues = trades + .map(t => t.maxFavorableExcursion!) + .filter(v => v !== null && v !== undefined) + .sort((a, b) => b - a) + + const avgMFE = mfeValues.reduce((sum, v) => sum + v, 0) / mfeValues.length + const medianMFE = mfeValues[Math.floor(mfeValues.length / 2)] + const percentile25MFE = mfeValues[Math.floor(mfeValues.length * 0.75)] // Reverse for MFE + const percentile75MFE = mfeValues[Math.floor(mfeValues.length * 0.25)] + const bestMFE = Math.max(...mfeValues) + + // Current configuration analysis (extract from first trade's config snapshot) + const sampleConfig: any = trades[0]?.configSnapshot || {} + const currentTP1 = sampleConfig.takeProfit1Percent || 0.4 + const currentTP2 = sampleConfig.takeProfit2Percent || 0.7 + const currentSL = sampleConfig.stopLossPercent || -1.1 + + // Calculate hit rates for current levels + const tp1Hits = trades.filter(t => { + const mfe = t.maxFavorableExcursion || 0 + return mfe >= currentTP1 + }).length + + const tp2Hits = trades.filter(t => { + const mfe = t.maxFavorableExcursion || 0 + return mfe >= currentTP2 + }).length + + const slHits = trades.filter(t => { + const mae = t.maxAdverseExcursion || 0 + return mae <= currentSL + }).length + + const tp1HitRate = (tp1Hits / trades.length) * 100 + const tp2HitRate = (tp2Hits / trades.length) * 100 + const slHitRate = (slHits / trades.length) * 100 + + // Calculate "money left on table" - how much profit we didn't capture + const moneyLeftOnTable = winningTrades.reduce((sum, t) => { + const mfe = t.maxFavorableExcursion || 0 + const realizedPct = ((t.realizedPnL || 0) / t.positionSizeUSD) * 100 + const leftOnTable = Math.max(0, mfe - realizedPct) + return sum + (leftOnTable * t.positionSizeUSD / 100) + }, 0) + + // Calculate optimal levels + const optimalTP1 = avgMFE * 0.5 // Capture 50% of avg move + const optimalTP2 = avgMFE * 0.8 // Capture 80% of avg move + const optimalSL = avgMAE * 0.7 // Exit at 70% of avg adverse move (tighter to minimize losses) + + // Trade outcome breakdown + const tp1Exits = trades.filter(t => t.exitReason === 'TP1').length + const tp2Exits = trades.filter(t => t.exitReason === 'TP2').length + const slExits = trades.filter(t => + t.exitReason === 'SL' || t.exitReason === 'SOFT_SL' || t.exitReason === 'HARD_SL' + ).length + const manualExits = trades.filter(t => + t.exitReason === 'manual' || t.exitReason === 'emergency' + ).length + + // Projected impact calculation + // Simulate what would have happened with optimal levels + let projectedWins = 0 + let projectedLosses = 0 + let projectedTotalPnL = 0 + + trades.forEach(t => { + const mfe = t.maxFavorableExcursion || 0 + const mae = t.maxAdverseExcursion || 0 + + // Would SL have been hit first with optimal level? + if (mae <= optimalSL) { + projectedLosses++ + projectedTotalPnL += optimalSL * t.positionSizeUSD / 100 + } + // Would TP1 have been hit? + else if (mfe >= optimalTP1) { + projectedWins++ + // Assume 50% exit at TP1, 50% continues to TP2 or SL + const tp1PnL = optimalTP1 * t.positionSizeUSD * 0.5 / 100 + + if (mfe >= optimalTP2) { + const tp2PnL = optimalTP2 * t.positionSizeUSD * 0.5 / 100 + projectedTotalPnL += tp1PnL + tp2PnL + } else { + // TP2 not hit, remaining 50% exits at breakeven or small profit + projectedTotalPnL += tp1PnL + } + } + }) + + const projectedWinRate = (projectedWins / trades.length) * 100 + const expectedWinRateChange = projectedWinRate - winRate + + const projectedProfitFactor = projectedLosses > 0 + ? (projectedWins * avgWin) / (projectedLosses * avgLoss) + : 0 + const expectedProfitFactorChange = projectedProfitFactor - profitFactor + + const estimatedProfitImprovement = totalPnL > 0 + ? ((projectedTotalPnL - totalPnL) / totalPnL) * 100 + : 0 + + // Build response + const analysis: TPSLOptimizationResponse = { + success: true, + analysis: { + totalTrades: trades.length, + winningTrades: winningTrades.length, + losingTrades: losingTrades.length, + winRate, + avgWin, + avgLoss, + profitFactor, + + maeAnalysis: { + avgMAE, + medianMAE, + percentile25MAE, + percentile75MAE, + worstMAE, + }, + + mfeAnalysis: { + avgMFE, + medianMFE, + percentile25MFE, + percentile75MFE, + bestMFE, + }, + + currentLevels: { + tp1Percent: currentTP1, + tp2Percent: currentTP2, + slPercent: currentSL, + tp1HitRate, + tp2HitRate, + slHitRate, + moneyLeftOnTable, + }, + + recommendations: { + optimalTP1, + optimalTP2, + optimalSL, + + reasoning: { + tp1: `Set at ${optimalTP1.toFixed(2)}% (50% of avg MFE ${avgMFE.toFixed(2)}%). This captures early profits while letting winners run. Current hit rate: ${tp1HitRate.toFixed(1)}%`, + tp2: `Set at ${optimalTP2.toFixed(2)}% (80% of avg MFE ${avgMFE.toFixed(2)}%). This captures most of the move before reversal. Current hit rate: ${tp2HitRate.toFixed(1)}%`, + sl: `Set at ${optimalSL.toFixed(2)}% (70% of avg MAE ${avgMAE.toFixed(2)}%). Tighter stop to minimize losses on bad trades. Current hit rate: ${slHitRate.toFixed(1)}%`, + }, + + projectedImpact: { + expectedWinRateChange, + expectedProfitFactorChange, + estimatedProfitImprovement, + }, + }, + + tradesByOutcome: { + tp1Exits, + tp2Exits, + slExits, + manualExits, + }, + }, + } + + console.log('✅ TP/SL optimization analysis complete') + console.log(' Current: TP1=' + currentTP1 + '% TP2=' + currentTP2 + '% SL=' + currentSL + '%') + console.log(' Optimal: TP1=' + optimalTP1.toFixed(2) + '% TP2=' + optimalTP2.toFixed(2) + '% SL=' + optimalSL.toFixed(2) + '%') + console.log(' Projected improvement: ' + estimatedProfitImprovement.toFixed(1) + '%') + + return NextResponse.json(analysis) + + } catch (error) { + console.error('❌ TP/SL optimization error:', error) + + return NextResponse.json( + { + success: false, + error: 'Failed to analyze trades: ' + (error as Error).message, + }, + { status: 500 } + ) + } +} diff --git a/scripts/test-analytics.sh b/scripts/test-analytics.sh new file mode 100755 index 0000000..67ade9d --- /dev/null +++ b/scripts/test-analytics.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Test TP/SL Optimization Analytics Endpoint +# Usage: ./scripts/test-analytics.sh + +echo "🔍 Testing TP/SL Optimization Analytics..." +echo "" + +RESPONSE=$(curl -s http://localhost:3001/api/analytics/tp-sl-optimization) + +# Pretty print JSON response +echo "$RESPONSE" | jq '.' || echo "$RESPONSE" + +echo "" +echo "✅ Test complete"