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:
@@ -67,10 +67,34 @@ interface PositionSummary {
|
|||||||
netPositions: NetPosition[]
|
netPositions: NetPosition[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VersionComparison {
|
||||||
|
versions: VersionStats[]
|
||||||
|
descriptions: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
export default function AnalyticsPage() {
|
export default function AnalyticsPage() {
|
||||||
const [stats, setStats] = useState<Stats | null>(null)
|
const [stats, setStats] = useState<Stats | null>(null)
|
||||||
const [positions, setPositions] = useState<PositionSummary | null>(null)
|
const [positions, setPositions] = useState<PositionSummary | null>(null)
|
||||||
const [lastTrade, setLastTrade] = useState<LastTrade | null>(null)
|
const [lastTrade, setLastTrade] = useState<LastTrade | null>(null)
|
||||||
|
const [versionComparison, setVersionComparison] = useState<VersionComparison | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [selectedDays, setSelectedDays] = useState(30)
|
const [selectedDays, setSelectedDays] = useState(30)
|
||||||
|
|
||||||
@@ -81,19 +105,22 @@ export default function AnalyticsPage() {
|
|||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const [statsRes, positionsRes, lastTradeRes] = await Promise.all([
|
const [statsRes, positionsRes, lastTradeRes, versionRes] = await Promise.all([
|
||||||
fetch(`/api/analytics/stats?days=${selectedDays}`),
|
fetch(`/api/analytics/stats?days=${selectedDays}`),
|
||||||
fetch('/api/analytics/positions'),
|
fetch('/api/analytics/positions'),
|
||||||
fetch('/api/analytics/last-trade'),
|
fetch('/api/analytics/last-trade'),
|
||||||
|
fetch('/api/analytics/version-comparison'),
|
||||||
])
|
])
|
||||||
|
|
||||||
const statsData = await statsRes.json()
|
const statsData = await statsRes.json()
|
||||||
const positionsData = await positionsRes.json()
|
const positionsData = await positionsRes.json()
|
||||||
const lastTradeData = await lastTradeRes.json()
|
const lastTradeData = await lastTradeRes.json()
|
||||||
|
const versionData = await versionRes.json()
|
||||||
|
|
||||||
setStats(statsData.stats)
|
setStats(statsData.stats)
|
||||||
setPositions(positionsData.summary)
|
setPositions(positionsData.summary)
|
||||||
setLastTrade(lastTradeData.trade)
|
setLastTrade(lastTradeData.trade)
|
||||||
|
setVersionComparison(versionData.success ? versionData : null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load analytics:', error)
|
console.error('Failed to load analytics:', error)
|
||||||
}
|
}
|
||||||
@@ -250,6 +277,180 @@ export default function AnalyticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Signal Quality Version Comparison */}
|
||||||
|
{versionComparison && versionComparison.versions.length > 0 && (
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-xl font-bold text-white mb-4">🔬 Signal Quality Logic Versions</h2>
|
||||||
|
<div className="bg-gray-800/50 backdrop-blur-sm rounded-xl p-6 border border-gray-700">
|
||||||
|
<p className="text-gray-300 text-sm mb-6 leading-relaxed">
|
||||||
|
The bot has evolved through different signal quality scoring algorithms.
|
||||||
|
This section compares their performance to enable data-driven optimization.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{versionComparison.versions.map((version, idx) => {
|
||||||
|
const isCurrentVersion = version.version === 'v3'
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={version.version}
|
||||||
|
className={`p-5 rounded-lg border ${isCurrentVersion ? 'bg-blue-900/20 border-blue-500/50' : 'bg-gray-700/30 border-gray-600'}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center space-x-3 mb-2">
|
||||||
|
<h3 className={`text-lg font-bold ${isCurrentVersion ? 'text-blue-400' : 'text-white'}`}>
|
||||||
|
{version.version.toUpperCase()}
|
||||||
|
{isCurrentVersion && (
|
||||||
|
<span className="ml-2 px-2 py-1 text-xs bg-blue-600 text-white rounded-full">
|
||||||
|
CURRENT
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
{versionComparison.descriptions[version.version] || 'Unknown version'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Metrics Grid */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
||||||
|
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">Trades</div>
|
||||||
|
<div className="text-xl font-bold text-white">{version.tradeCount}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">Win Rate</div>
|
||||||
|
<div className={`text-xl font-bold ${version.winRate >= 50 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{version.winRate}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">Total P&L</div>
|
||||||
|
<div className={`text-xl font-bold ${version.totalPnL >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{version.totalPnL >= 0 ? '+' : ''}${version.totalPnL.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">Avg P&L</div>
|
||||||
|
<div className={`text-xl font-bold ${version.avgPnL >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{version.avgPnL >= 0 ? '+' : ''}${version.avgPnL.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Advanced Metrics */}
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 mb-4">
|
||||||
|
{version.avgQualityScore !== null && (
|
||||||
|
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">Avg Quality Score</div>
|
||||||
|
<div className={`text-lg font-semibold ${version.avgQualityScore >= 75 ? 'text-green-400' : 'text-yellow-400'}`}>
|
||||||
|
{version.avgQualityScore}/100
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{version.avgMFE !== null && (
|
||||||
|
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">Avg MFE</div>
|
||||||
|
<div className="text-lg font-semibold text-green-400">
|
||||||
|
+{version.avgMFE.toFixed(2)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{version.avgMAE !== null && (
|
||||||
|
<div className="bg-gray-800/50 rounded-lg p-3">
|
||||||
|
<div className="text-xs text-gray-400 mb-1">Avg MAE</div>
|
||||||
|
<div className="text-lg font-semibold text-red-400">
|
||||||
|
{version.avgMAE.toFixed(2)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Extreme Position Stats */}
|
||||||
|
{version.extremePositions.count > 0 && (
|
||||||
|
<div className="pt-4 border-t border-gray-600/50">
|
||||||
|
<div className="text-xs text-gray-400 mb-3 flex items-center">
|
||||||
|
<span className="text-yellow-500 mr-2">⚠️</span>
|
||||||
|
Extreme Positions (< 15% or > 85% range)
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||||
|
<div className="bg-gray-800/50 rounded p-2">
|
||||||
|
<div className="text-xs text-gray-500">Count</div>
|
||||||
|
<div className="text-sm font-semibold text-white">
|
||||||
|
{version.extremePositions.count}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{version.extremePositions.avgADX !== null && (
|
||||||
|
<div className="bg-gray-800/50 rounded p-2">
|
||||||
|
<div className="text-xs text-gray-500">Avg ADX</div>
|
||||||
|
<div className={`text-sm font-semibold ${version.extremePositions.avgADX >= 18 ? 'text-green-400' : 'text-orange-400'}`}>
|
||||||
|
{version.extremePositions.avgADX.toFixed(1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="bg-gray-800/50 rounded p-2">
|
||||||
|
<div className="text-xs text-gray-500">Weak ADX</div>
|
||||||
|
<div className="text-sm font-semibold text-orange-400">
|
||||||
|
{version.extremePositions.weakADXCount}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800/50 rounded p-2">
|
||||||
|
<div className="text-xs text-gray-500">Win Rate</div>
|
||||||
|
<div className={`text-sm font-semibold ${version.extremePositions.winRate >= 50 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{version.extremePositions.winRate}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-800/50 rounded p-2">
|
||||||
|
<div className="text-xs text-gray-500">Avg P&L</div>
|
||||||
|
<div className={`text-sm font-semibold ${version.extremePositions.avgPnL >= 0 ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{version.extremePositions.avgPnL >= 0 ? '+' : ''}${version.extremePositions.avgPnL.toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Data Collection Notice for v3 */}
|
||||||
|
{isCurrentVersion && version.tradeCount < 20 && (
|
||||||
|
<div className="mt-4 p-3 bg-yellow-900/20 rounded-lg border border-yellow-500/30">
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<span className="text-yellow-500 text-sm">📊</span>
|
||||||
|
<p className="text-xs text-yellow-300/80 leading-relaxed">
|
||||||
|
<strong>Data Collection Phase:</strong> Need {20 - version.tradeCount} more trades
|
||||||
|
before v3 performance can be reliably evaluated. This version is designed to prevent
|
||||||
|
losses from extreme position entries with weak trends (ADX < 18).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Legend */}
|
||||||
|
<div className="mt-6 pt-6 border-t border-gray-600/50">
|
||||||
|
<div className="text-xs text-gray-400 space-y-1">
|
||||||
|
<div><strong className="text-gray-300">MFE (Max Favorable Excursion):</strong> Best profit % reached during trade lifetime</div>
|
||||||
|
<div><strong className="text-gray-300">MAE (Max Adverse Excursion):</strong> Worst loss % reached during trade lifetime</div>
|
||||||
|
<div><strong className="text-gray-300">Extreme Positions:</strong> Trades entered at price range extremes (< 15% or > 85%)</div>
|
||||||
|
<div><strong className="text-gray-300">Weak ADX:</strong> Trend strength below 18 (indicates sideways/choppy market)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Last Trade Details */}
|
{/* Last Trade Details */}
|
||||||
{lastTrade && (
|
{lastTrade && (
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
|||||||
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 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
124
docs/analysis/SIGNAL_QUALITY_VERSION_ANALYSIS.sql
Normal file
124
docs/analysis/SIGNAL_QUALITY_VERSION_ANALYSIS.sql
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
-- Signal Quality Version Analysis
|
||||||
|
-- Compare performance between different scoring logic versions
|
||||||
|
|
||||||
|
-- 1. Count trades by version
|
||||||
|
SELECT
|
||||||
|
COALESCE("signalQualityVersion", 'v1/null') as version,
|
||||||
|
COUNT(*) as trade_count,
|
||||||
|
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER(), 1) as percentage
|
||||||
|
FROM "Trade"
|
||||||
|
WHERE "exitReason" IS NOT NULL
|
||||||
|
GROUP BY "signalQualityVersion"
|
||||||
|
ORDER BY version;
|
||||||
|
|
||||||
|
-- 2. Performance by version
|
||||||
|
SELECT
|
||||||
|
COALESCE("signalQualityVersion", 'v1/null') as version,
|
||||||
|
COUNT(*) as trades,
|
||||||
|
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl,
|
||||||
|
ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl,
|
||||||
|
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate,
|
||||||
|
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%'
|
||||||
|
GROUP BY "signalQualityVersion"
|
||||||
|
ORDER BY version;
|
||||||
|
|
||||||
|
-- 3. Version breakdown by exit reason
|
||||||
|
SELECT
|
||||||
|
COALESCE("signalQualityVersion", 'v1/null') as version,
|
||||||
|
"exitReason",
|
||||||
|
COUNT(*) as count,
|
||||||
|
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl
|
||||||
|
FROM "Trade"
|
||||||
|
WHERE "exitReason" IS NOT NULL AND "exitReason" NOT LIKE '%CLEANUP%'
|
||||||
|
GROUP BY "signalQualityVersion", "exitReason"
|
||||||
|
ORDER BY version, count DESC;
|
||||||
|
|
||||||
|
-- 4. Quality score distribution by version
|
||||||
|
SELECT
|
||||||
|
COALESCE("signalQualityVersion", 'v1/null') as version,
|
||||||
|
CASE
|
||||||
|
WHEN "signalQualityScore" >= 80 THEN '80-100 (High)'
|
||||||
|
WHEN "signalQualityScore" >= 70 THEN '70-79 (Good)'
|
||||||
|
WHEN "signalQualityScore" >= 60 THEN '60-69 (Pass)'
|
||||||
|
ELSE '< 60 (Block)'
|
||||||
|
END as score_range,
|
||||||
|
COUNT(*) as trades,
|
||||||
|
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl,
|
||||||
|
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate
|
||||||
|
FROM "Trade"
|
||||||
|
WHERE "exitReason" IS NOT NULL AND "signalQualityScore" IS NOT NULL
|
||||||
|
GROUP BY "signalQualityVersion", score_range
|
||||||
|
ORDER BY version, score_range DESC;
|
||||||
|
|
||||||
|
-- 5. Extreme position entries by version (< 15% or > 85%)
|
||||||
|
SELECT
|
||||||
|
COALESCE("signalQualityVersion", 'v1/null') as version,
|
||||||
|
direction,
|
||||||
|
COUNT(*) as trades,
|
||||||
|
ROUND(AVG("pricePositionAtEntry")::numeric, 1) as avg_price_pos,
|
||||||
|
ROUND(AVG("adxAtEntry")::numeric, 1) as avg_adx,
|
||||||
|
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl,
|
||||||
|
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate
|
||||||
|
FROM "Trade"
|
||||||
|
WHERE "exitReason" IS NOT NULL
|
||||||
|
AND "pricePositionAtEntry" IS NOT NULL
|
||||||
|
AND ("pricePositionAtEntry" < 15 OR "pricePositionAtEntry" > 85)
|
||||||
|
GROUP BY "signalQualityVersion", direction
|
||||||
|
ORDER BY version, direction;
|
||||||
|
|
||||||
|
-- 6. Recent v3 trades (new logic)
|
||||||
|
SELECT
|
||||||
|
"createdAt",
|
||||||
|
symbol,
|
||||||
|
direction,
|
||||||
|
"entryPrice",
|
||||||
|
"exitPrice",
|
||||||
|
"exitReason",
|
||||||
|
ROUND("realizedPnL"::numeric, 2) as pnl,
|
||||||
|
"signalQualityScore" as score,
|
||||||
|
ROUND("adxAtEntry"::numeric, 1) as adx,
|
||||||
|
ROUND("pricePositionAtEntry"::numeric, 1) as price_pos
|
||||||
|
FROM "Trade"
|
||||||
|
WHERE "signalQualityVersion" = 'v3'
|
||||||
|
AND "exitReason" IS NOT NULL
|
||||||
|
ORDER BY "createdAt" DESC
|
||||||
|
LIMIT 20;
|
||||||
|
|
||||||
|
-- 7. Compare v3 vs pre-v3 on extreme positions
|
||||||
|
WITH version_groups AS (
|
||||||
|
SELECT
|
||||||
|
CASE WHEN "signalQualityVersion" = 'v3' THEN 'v3 (NEW)' ELSE 'pre-v3 (OLD)' END as version_group,
|
||||||
|
*
|
||||||
|
FROM "Trade"
|
||||||
|
WHERE "exitReason" IS NOT NULL
|
||||||
|
AND "pricePositionAtEntry" IS NOT NULL
|
||||||
|
AND ("pricePositionAtEntry" < 15 OR "pricePositionAtEntry" > 85)
|
||||||
|
AND "adxAtEntry" IS NOT NULL
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
version_group,
|
||||||
|
COUNT(*) as trades,
|
||||||
|
ROUND(AVG("adxAtEntry")::numeric, 1) as avg_adx,
|
||||||
|
COUNT(*) FILTER (WHERE "adxAtEntry" < 18) as weak_adx_count,
|
||||||
|
ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl,
|
||||||
|
ROUND(SUM("realizedPnL")::numeric, 2) as total_pnl,
|
||||||
|
ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate
|
||||||
|
FROM version_groups
|
||||||
|
GROUP BY version_group
|
||||||
|
ORDER BY version_group DESC;
|
||||||
|
|
||||||
|
-- 8. Daily performance by version (last 7 days)
|
||||||
|
SELECT
|
||||||
|
DATE("createdAt") as trade_date,
|
||||||
|
COALESCE("signalQualityVersion", 'v1/null') as version,
|
||||||
|
COUNT(*) as trades,
|
||||||
|
ROUND(SUM("realizedPnL")::numeric, 2) as daily_pnl
|
||||||
|
FROM "Trade"
|
||||||
|
WHERE "exitReason" IS NOT NULL
|
||||||
|
AND "createdAt" >= NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY trade_date, "signalQualityVersion"
|
||||||
|
ORDER BY trade_date DESC, version;
|
||||||
Reference in New Issue
Block a user