Phase 3: TP/SL Optimization Analytics API
- Created /api/analytics/tp-sl-optimization endpoint - Analyzes historical trades using MAE/MFE data - Calculates optimal TP1/TP2/SL levels based on percentiles - Provides win rate, profit factor, and hit rate analysis - Shows money left on table (MFE - realized P&L) - Projects impact of optimal levels on future performance Analytics calculated: - MAE analysis: avg, median, percentiles, worst - MFE analysis: avg, median, percentiles, best - Current level performance: TP1/TP2/SL hit rates - Optimal recommendations: TP1=50% of avg MFE, TP2=80%, SL=70% of avg MAE - Projected improvements: win rate change, profit factor, total P&L Requires 10+ closed trades with MAE/MFE data to generate recommendations Test script: scripts/test-analytics.sh Next: Phase 4 (visual dashboard) or wait for trades with MAE/MFE data
This commit is contained in:
319
app/api/analytics/tp-sl-optimization/route.ts
Normal file
319
app/api/analytics/tp-sl-optimization/route.ts
Normal file
@@ -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<NextResponse<TPSLOptimizationResponse>> {
|
||||||
|
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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
15
scripts/test-analytics.sh
Executable file
15
scripts/test-analytics.sh
Executable file
@@ -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"
|
||||||
Reference in New Issue
Block a user