/** * 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 } // Dynamic ATR Analysis (v6 trades only) dynamicATRAnalysis?: { available: boolean sampleSize: number minSampleSize: number sufficientData: boolean avgATRPercent: number // Dynamic targets dynamicTP2Percent: number // 2x ATR dynamicSLPercent: number // 1.5x ATR // Simulated P&L comparison actualPnL: number fixedSimulatedPnL: number dynamicSimulatedPnL: number dynamicAdvantage: number dynamicAdvantagePercent: number // Hit rates dynamicTP2HitRate: number dynamicSLHitRate: number // Recommendation recommendation: string reasoning: string } } 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 // === DYNAMIC ATR ANALYSIS (v6 trades only) === const v6Trades = trades.filter(t => t.indicatorVersion === 'v6' && t.atrAtEntry !== null && t.atrAtEntry !== undefined ) let dynamicATRAnalysis = undefined const minSampleSize = 30 if (v6Trades.length > 0) { console.log(`📊 Analyzing ${v6Trades.length} v6 trades for dynamic ATR-based TP/SL`) // Calculate ATR-based targets const atrAnalysis = v6Trades.map(t => { const atrPercent = (t.atrAtEntry! / t.entryPrice) * 100 const dynamicTP2 = atrPercent * 2 // 2x ATR const dynamicSL = atrPercent * 1.5 // 1.5x ATR const mfe = t.maxFavorableExcursion || 0 const mae = t.maxAdverseExcursion || 0 // Simulate outcomes const wouldHitDynamicTP2 = mfe >= dynamicTP2 const wouldHitDynamicSL = mae <= -dynamicSL // Calculate simulated P&L let dynamicPnL = t.realizedPnL || 0 if (wouldHitDynamicTP2) { // Hit TP2 at 2x ATR dynamicPnL = t.positionSizeUSD * dynamicTP2 / 100 } else if (wouldHitDynamicSL) { // Hit SL at 1.5x ATR dynamicPnL = -t.positionSizeUSD * dynamicSL / 100 } let fixedPnL = t.realizedPnL || 0 if (mfe >= currentTP2) { fixedPnL = t.positionSizeUSD * currentTP2 / 100 } else if (mae <= currentSL) { fixedPnL = t.positionSizeUSD * currentSL / 100 } return { atrPercent, dynamicTP2, dynamicSL, wouldHitDynamicTP2, wouldHitDynamicSL, dynamicPnL, fixedPnL, actualPnL: t.realizedPnL || 0 } }) const avgATRPercent = atrAnalysis.reduce((sum, a) => sum + a.atrPercent, 0) / atrAnalysis.length const avgDynamicTP2 = atrAnalysis.reduce((sum, a) => sum + a.dynamicTP2, 0) / atrAnalysis.length const avgDynamicSL = atrAnalysis.reduce((sum, a) => sum + a.dynamicSL, 0) / atrAnalysis.length const totalActualPnL = atrAnalysis.reduce((sum, a) => sum + a.actualPnL, 0) const totalFixedPnL = atrAnalysis.reduce((sum, a) => sum + a.fixedPnL, 0) const totalDynamicPnL = atrAnalysis.reduce((sum, a) => sum + a.dynamicPnL, 0) const dynamicAdvantage = totalDynamicPnL - totalFixedPnL const dynamicAdvantagePercent = totalFixedPnL !== 0 ? (dynamicAdvantage / Math.abs(totalFixedPnL)) * 100 : 0 const dynamicTP2Hits = atrAnalysis.filter(a => a.wouldHitDynamicTP2).length const dynamicSLHits = atrAnalysis.filter(a => a.wouldHitDynamicSL).length const sufficientData = v6Trades.length >= minSampleSize let recommendation = '' let reasoning = '' if (!sufficientData) { recommendation = 'WAIT - Need more data' reasoning = `Only ${v6Trades.length}/${minSampleSize} trades collected. Continue using fixed targets until we have ${minSampleSize}+ v6 trades for statistical significance.` } else if (dynamicAdvantagePercent > 20) { recommendation = 'IMPLEMENT - Strong advantage' reasoning = `Dynamic ATR-based targets show ${dynamicAdvantagePercent.toFixed(1)}% better performance over ${v6Trades.length} trades. The tighter SL (${avgDynamicSL.toFixed(2)}% vs ${Math.abs(currentSL)}%) reduces losses significantly.` } else if (dynamicAdvantagePercent > 10) { recommendation = 'CONSIDER - Moderate advantage' reasoning = `Dynamic ATR-based targets show ${dynamicAdvantagePercent.toFixed(1)}% improvement. Worth testing with smaller position sizes first.` } else if (dynamicAdvantagePercent > 0) { recommendation = 'NEUTRAL - Slight advantage' reasoning = `Dynamic targets show only ${dynamicAdvantagePercent.toFixed(1)}% improvement. May not be worth the added complexity.` } else { recommendation = 'DO NOT IMPLEMENT' reasoning = `Dynamic targets underperform fixed targets by ${Math.abs(dynamicAdvantagePercent).toFixed(1)}%. Stick with current fixed levels.` } dynamicATRAnalysis = { available: true, sampleSize: v6Trades.length, minSampleSize, sufficientData, avgATRPercent, dynamicTP2Percent: avgDynamicTP2, dynamicSLPercent: avgDynamicSL, actualPnL: totalActualPnL, fixedSimulatedPnL: totalFixedPnL, dynamicSimulatedPnL: totalDynamicPnL, dynamicAdvantage, dynamicAdvantagePercent, dynamicTP2HitRate: (dynamicTP2Hits / v6Trades.length) * 100, dynamicSLHitRate: (dynamicSLHits / v6Trades.length) * 100, recommendation, reasoning } console.log(`✅ Dynamic ATR analysis: ${recommendation}`) console.log(` Sample: ${v6Trades.length}/${minSampleSize} trades`) console.log(` Advantage: ${dynamicAdvantagePercent.toFixed(1)}% (${dynamicAdvantage >= 0 ? '+' : ''}$${dynamicAdvantage.toFixed(2)})`) } // 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, }, dynamicATRAnalysis, }, } 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 } ) } }