Critical Bug Fix: - archivedVersions was used before declaration (line 147 vs line 165) - Caused 'Cannot access before initialization' error - Moved versionDescriptions and archivedVersions declarations to top - Now defined BEFORE usage in resultsWithArchived.map() Impact: Analytics page was completely broken (stuck on loading) Resolution: API now returns data correctly, UI functional Error: ReferenceError: Cannot access 'g' before initialization Fix: Proper variable ordering in route.ts
165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
/**
|
|
* Trading Bot v4 - Indicator Version Comparison API
|
|
*
|
|
* Primary: v8 Money Line (Nov 18+) - Production system
|
|
* Archived: v5/v6/unknown - Historical baseline for comparison
|
|
*
|
|
* Returns performance metrics for statistical validation and future v9 testing
|
|
*/
|
|
|
|
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: any
|
|
avg_pnl: any
|
|
avg_quality_score: any
|
|
avg_mfe: any
|
|
avg_mae: any
|
|
}>>`
|
|
SELECT
|
|
COALESCE("indicatorVersion", 'unknown') as version,
|
|
COUNT(*) as trades,
|
|
SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) as wins,
|
|
SUM("realizedPnL") as total_pnl,
|
|
AVG("realizedPnL") as avg_pnl,
|
|
AVG("realizedPnL") 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
|
|
AND ("signalSource" IS NULL OR "signalSource" != 'manual')
|
|
GROUP BY "indicatorVersion"
|
|
ORDER BY version DESC
|
|
`
|
|
|
|
// Get extreme position stats by version (< 15% price position)
|
|
const extremePositionStats = await prisma.$queryRaw<Array<{
|
|
version: string | null
|
|
trades: bigint
|
|
wins: bigint
|
|
total_pnl: any
|
|
avg_quality_score: any
|
|
}>>`
|
|
SELECT
|
|
COALESCE("indicatorVersion", 'unknown') as version,
|
|
COUNT(*) as trades,
|
|
SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) as wins,
|
|
SUM("realizedPnL") as total_pnl,
|
|
ROUND(AVG("signalQualityScore")::numeric, 1) as avg_quality_score
|
|
FROM "Trade"
|
|
WHERE "exitReason" IS NOT NULL
|
|
AND "exitReason" NOT LIKE '%CLEANUP%'
|
|
AND "isTestTrade" = false
|
|
AND ("signalSource" IS NULL OR "signalSource" != 'manual')
|
|
AND "pricePositionAtEntry" < 15
|
|
GROUP BY "indicatorVersion"
|
|
ORDER BY version DESC
|
|
`
|
|
|
|
// Build combined results
|
|
const results: VersionStats[] = versionStats.map(stat => {
|
|
const extremeStats = extremePositionStats.find(e =>
|
|
(e.version || 'unknown') === (stat.version || 'unknown')
|
|
)
|
|
|
|
const trades = Number(stat.trades)
|
|
const wins = Number(stat.wins)
|
|
const extremeCount = extremeStats ? Number(extremeStats.trades) : 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: Number(stat.total_pnl) || 0,
|
|
avgPnL: Number(stat.avg_pnl) || 0,
|
|
avgQualityScore: stat.avg_quality_score ? Number(stat.avg_quality_score) : null,
|
|
avgMFE: stat.avg_mfe ? Number(stat.avg_mfe) : null,
|
|
avgMAE: stat.avg_mae ? Number(stat.avg_mae) : null,
|
|
extremePositions: {
|
|
count: extremeCount,
|
|
avgADX: null,
|
|
weakADXCount: 0,
|
|
winRate: extremeCount > 0 ? Math.round((extremeWins / extremeCount) * 100 * 10) / 10 : 0,
|
|
avgPnL: extremeStats?.total_pnl ? Number(extremeStats.total_pnl) / extremeCount : 0,
|
|
}
|
|
}
|
|
})
|
|
|
|
// Define version metadata FIRST (before usage)
|
|
const versionDescriptions: Record<string, string> = {
|
|
'v8': 'Money Line Sticky Trend (Nov 18+) - PRODUCTION',
|
|
'v7': 'HalfTrend with toggles (deprecated)',
|
|
'v6': 'HalfTrend + BarColor (Nov 12-18) - ARCHIVED',
|
|
'v5': 'Buy/Sell Signal (pre-Nov 12) - ARCHIVED',
|
|
'unknown': 'No version tracked (pre-Nov 12) - ARCHIVED'
|
|
}
|
|
|
|
const archivedVersions = ['v5', 'v6', 'v7', 'unknown']
|
|
|
|
// Sort versions: v8 first (production), then v7, v6, v5, unknown (archived)
|
|
const versionOrder: Record<string, number> = {
|
|
'v8': 0, 'v7': 1, 'v6': 2, 'v5': 3, 'unknown': 4
|
|
}
|
|
results.sort((a, b) => {
|
|
const orderA = versionOrder[a.version] ?? 999
|
|
const orderB = versionOrder[b.version] ?? 999
|
|
return orderA - orderB
|
|
})
|
|
|
|
// Mark archived versions (archivedVersions now defined above)
|
|
const resultsWithArchived = results.map(r => ({
|
|
...r,
|
|
archived: archivedVersions.includes(r.version)
|
|
}))
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
versions: resultsWithArchived,
|
|
descriptions: versionDescriptions,
|
|
production: 'v8',
|
|
archived: archivedVersions,
|
|
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 }
|
|
)
|
|
}
|
|
}
|