Add signal quality version comparison to analytics dashboard
- Created /api/analytics/version-comparison endpoint - Shows performance metrics for v1, v2, v3 scoring logic - Compares: trade count, win rate, P&L, quality scores, MFE/MAE - Special focus on extreme positions (< 15% or > 85% range) - Tracks weak ADX count (< 18) for each version - Visual indicators for current version (v3) - Data collection progress notice for v3 (need 20+ trades) - Legend explaining MFE, MAE, extreme positions, weak ADX Enables data-driven optimization by comparing algorithm performance with clean, version-tagged datasets.
This commit is contained in:
139
app/api/analytics/version-comparison/route.ts
Normal file
139
app/api/analytics/version-comparison/route.ts
Normal file
@@ -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<Array<{
|
||||
version: string | null
|
||||
trades: bigint
|
||||
wins: bigint
|
||||
total_pnl: number
|
||||
avg_pnl: number
|
||||
avg_quality_score: number | null
|
||||
avg_mfe: number | null
|
||||
avg_mae: number | null
|
||||
}>>`
|
||||
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<Array<{
|
||||
version: string | null
|
||||
count: bigint
|
||||
avg_adx: number | null
|
||||
weak_adx_count: bigint
|
||||
wins: bigint
|
||||
avg_pnl: number
|
||||
}>>`
|
||||
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<string, string> = {
|
||||
'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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user