/home/devscfvi/web.devsquantum.com/js/analytics.js
// ============================================
// ADVANCED ANALYTICS MODULE - COMPLETE
// ============================================
// Render Analytics Dashboard
function renderAnalytics() {
if (!state.analytics) {
return `
<div class="min-h-screen p-4">
<div class="max-w-2xl mx-auto">
<div class="mb-6 pt-4">
<h1 class="text-2xl font-bold text-white">Analytics</h1>
<p class="text-violet-300">Loading insights...</p>
</div>
<div class="flex items-center justify-center py-12">
<div class="spinner"></div>
</div>
</div>
</div>
`;
}
const analytics = state.analytics;
const hasData = state.trades && state.trades.length > 0;
return `
<div class="min-h-screen p-4">
<div class="max-w-4xl mx-auto">
<!-- Header -->
<div class="mb-6 pt-4">
<h1 class="text-2xl font-bold text-white mb-1">Advanced Analytics</h1>
<p class="text-sm text-violet-300">Deep insights into your trading performance</p>
</div>
${!hasData ? renderEmptyAnalytics() : `
<!-- Tab Navigation -->
<div class="glass rounded-2xl p-2 mb-6 border border-violet-500/20">
<div class="grid grid-cols-5 gap-1.5">
<button onclick="switchAnalyticsTab('overview')"
id="tab-overview"
class="analytics-tab py-3 rounded-xl font-semibold transition text-xs md:text-sm">
Overview
</button>
<button onclick="switchAnalyticsTab('category')"
id="tab-category"
class="analytics-tab py-3 rounded-xl font-semibold transition text-xs md:text-sm">
Strategy
</button>
<button onclick="switchAnalyticsTab('psychology')"
id="tab-psychology"
class="analytics-tab py-3 rounded-xl font-semibold transition text-xs md:text-sm">
Psychology
</button>
<button onclick="switchAnalyticsTab('timing')"
id="tab-timing"
class="analytics-tab py-3 rounded-xl font-semibold transition text-xs md:text-sm">
Timing
</button>
<button onclick="switchAnalyticsTab('reports')"
id="tab-reports"
class="analytics-tab py-3 rounded-xl font-semibold transition text-xs md:text-sm">
Reports
</button>
</div>
</div>
<!-- Tab Content -->
<div id="analytics-content"></div>
`}
</div>
</div>
`;
}
// Empty state
function renderEmptyAnalytics() {
return `
<div class="glass rounded-3xl p-12 text-center border border-violet-500/20">
<svg class="w-16 h-16 mx-auto text-violet-400/50 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
<p class="text-lg text-white mb-2">No Data Yet</p>
<p class="text-sm text-slate-400 mb-4">Start tracking trades to see analytics</p>
<button onclick="navigateTo('calculator')"
class="gradient-bg text-white px-6 py-3 rounded-xl font-semibold active:scale-95 transition">
Create First Trade
</button>
</div>
`;
}
// Switch between analytics tabs
let currentAnalyticsTab = 'overview';
function switchAnalyticsTab(tab) {
currentAnalyticsTab = tab;
// Update tab buttons
document.querySelectorAll('.analytics-tab').forEach(btn => {
const btnTab = btn.id.replace('tab-', '');
if (btnTab === tab) {
btn.classList.add('bg-violet-600', 'text-white');
btn.classList.remove('bg-slate-900/50', 'text-slate-400');
} else {
btn.classList.remove('bg-violet-600', 'text-white');
btn.classList.add('bg-slate-900/50', 'text-slate-400');
}
});
// Render content
const container = document.getElementById('analytics-content');
if (!container) return;
switch(tab) {
case 'overview':
container.innerHTML = renderOverviewTab();
setTimeout(() => initOverviewCharts(), 100);
break;
case 'category':
container.innerHTML = renderCategoryTab();
setTimeout(() => initCategoryCharts(), 100);
break;
case 'psychology':
container.innerHTML = renderPsychologyTab();
setTimeout(() => initPsychologyCharts(), 100);
break;
case 'timing':
container.innerHTML = renderTimingTab();
setTimeout(() => initTimingCharts(), 100);
break;
case 'reports':
container.innerHTML = renderReportsTab();
setTimeout(() => initReportsCharts(), 100);
break;
}
}
// ============================================
// OVERVIEW TAB
// ============================================
function renderOverviewTab() {
const analytics = state.analytics;
const streaks = analytics.streaks;
const stats = state.stats || {};
return `
<div class="space-y-4">
<!-- Quick Stats Card -->
<div class="glass rounded-2xl p-4 border border-violet-500/20">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-violet-300">Quick Stats</h3>
<button onclick="navigateTo('stats')"
class="text-xs text-violet-400 hover:text-violet-300 transition flex items-center gap-1">
View Full Stats
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</button>
</div>
<div class="grid grid-cols-4 gap-3">
<div class="text-center">
<p class="text-2xl font-bold text-white">${stats.total_trades || 0}</p>
<p class="text-xs text-slate-400">Trades</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold ${parseFloat(stats.win_rate || 0) >= 50 ? 'text-green-400' : 'text-red-400'}">
${stats.win_rate || 0}%
</p>
<p class="text-xs text-slate-400">Win Rate</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold ${parseFloat(stats.net_pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}">
${parseFloat(stats.net_pnl || 0) >= 0 ? '+' : ''}${formatCurrency(stats.net_pnl || 0)}
</p>
<p class="text-xs text-slate-400">Net P&L</p>
</div>
<div class="text-center">
<p class="text-2xl font-bold text-violet-400">1:${parseFloat(stats.avg_rr || 0).toFixed(2)}</p>
<p class="text-xs text-slate-400">Avg R:R</p>
</div>
</div>
</div>
<!-- Streaks -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">Current Streaks</h3>
<div class="grid grid-cols-2 gap-4">
<div class="bg-green-500/10 rounded-2xl p-4 border border-green-500/20">
<p class="text-xs text-green-300 mb-1">Current Win Streak</p>
<p class="text-4xl font-bold text-green-400">${streaks.current_win_streak || 0}</p>
<p class="text-xs text-slate-400 mt-2">Best: ${streaks.max_win_streak || 0}</p>
</div>
<div class="bg-red-500/10 rounded-2xl p-4 border border-red-500/20">
<p class="text-xs text-red-300 mb-1">Current Loss Streak</p>
<p class="text-4xl font-bold text-red-400">${streaks.current_loss_streak || 0}</p>
<p class="text-xs text-slate-400 mt-2">Worst: ${streaks.max_loss_streak || 0}</p>
</div>
</div>
</div>
<!-- Top Performing Coins -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">Performance by Coin</h3>
${analytics.byCoin && analytics.byCoin.length > 0 ? `
<div class="space-y-3">
${analytics.byCoin.slice(0, 5).map(coin => `
<div class="bg-slate-900/50 rounded-xl p-4 border border-violet-500/20">
<div class="flex items-center justify-between mb-2">
<span class="text-lg font-bold text-white">${coin.coin}</span>
<span class="text-sm font-semibold ${parseFloat(coin.net_pnl) >= 0 ? 'text-green-400' : 'text-red-400'}">
${parseFloat(coin.net_pnl) >= 0 ? '+' : ''}${formatCurrency(coin.net_pnl)}
</span>
</div>
<div class="grid grid-cols-3 gap-2 text-xs">
<div>
<p class="text-slate-400">Trades</p>
<p class="text-white font-semibold">${coin.total_trades}</p>
</div>
<div>
<p class="text-slate-400">Win Rate</p>
<p class="text-white font-semibold">${coin.win_rate || 0}%</p>
</div>
<div>
<p class="text-slate-400">Avg R:R</p>
<p class="text-white font-semibold">1:${parseFloat(coin.avg_rr || 0).toFixed(2)}</p>
</div>
</div>
</div>
`).join('')}
</div>
` : '<p class="text-slate-400 text-center py-4">No data available</p>'}
</div>
<!-- Performance Chart -->
${analytics.byCoin && analytics.byCoin.length > 0 ? `
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">Win Rate by Coin</h3>
<div style="height: 300px;">
<canvas id="coinWinRateChart"></canvas>
</div>
</div>
` : ''}
</div>
`;
}
function initOverviewCharts() {
const analytics = state.analytics;
if (!analytics.byCoin || analytics.byCoin.length === 0) return;
const canvas = document.getElementById('coinWinRateChart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (window.coinWinRateChart && typeof window.coinWinRateChart.destroy === 'function') {
window.coinWinRateChart.destroy();
}
const topCoins = analytics.byCoin.slice(0, 10);
window.coinWinRateChart = new Chart(ctx, {
type: 'bar',
data: {
labels: topCoins.map(c => c.coin),
datasets: [{
label: 'Win Rate %',
data: topCoins.map(c => parseFloat(c.win_rate || 0)),
backgroundColor: topCoins.map(c => parseFloat(c.win_rate || 0) >= 50 ? '#10b981' : '#ef4444'),
borderRadius: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
callbacks: {
label: (context) => context.parsed.y.toFixed(1) + '%'
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
grid: { color: 'rgba(139, 92, 246, 0.1)' },
ticks: {
color: '#a78bfa',
callback: (value) => value + '%'
}
},
x: {
grid: { display: false },
ticks: { color: '#a78bfa' }
}
}
}
});
}
// ============================================
// CATEGORY TAB
// ============================================
function renderCategoryTab() {
const analytics = state.analytics;
return `
<div class="space-y-4">
<!-- Category Performance -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">Strategy Performance</h3>
${analytics.byCategory && analytics.byCategory.length > 0 ? `
<div class="space-y-3">
${analytics.byCategory.map(cat => `
<div class="bg-slate-900/50 rounded-xl p-4 border border-violet-500/20">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<span class="category-badge category-${cat.trade_category}">
${getCategoryLabel(cat.trade_category)}
</span>
<span class="text-sm text-slate-400">${cat.total_trades} trades</span>
</div>
<span class="text-lg font-bold ${parseFloat(cat.net_pnl) >= 0 ? 'text-green-400' : 'text-red-400'}">
${parseFloat(cat.net_pnl) >= 0 ? '+' : ''}${formatCurrency(cat.net_pnl)}
</span>
</div>
<div class="grid grid-cols-3 gap-3 text-sm">
<div>
<p class="text-slate-400 text-xs">Win Rate</p>
<p class="text-white font-semibold">${cat.win_rate || 0}%</p>
</div>
<div>
<p class="text-slate-400 text-xs">W/L</p>
<p class="text-white font-semibold">${cat.wins}/${cat.losses}</p>
</div>
<div>
<p class="text-slate-400 text-xs">Avg R:R</p>
<p class="text-white font-semibold">1:${parseFloat(cat.avg_rr || 0).toFixed(2)}</p>
</div>
</div>
</div>
`).join('')}
</div>
` : '<p class="text-slate-400 text-center py-8">No category data yet. Start adding categories to your trades!</p>'}
</div>
${analytics.byCategory && analytics.byCategory.length > 0 ? `
<!-- Category Charts -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">P&L by Strategy</h3>
<div style="height: 250px;">
<canvas id="categoryPnlChart"></canvas>
</div>
</div>
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">Trade Distribution</h3>
<div style="height: 250px;">
<canvas id="categoryDistChart"></canvas>
</div>
</div>
</div>
<!-- Best/Worst Strategy -->
<div class="grid grid-cols-2 gap-4">
${(() => {
const sorted = [...analytics.byCategory].sort((a, b) => parseFloat(b.net_pnl) - parseFloat(a.net_pnl));
const best = sorted[0];
const worst = sorted[sorted.length - 1];
return `
<div class="glass rounded-2xl p-5 border border-green-500/20">
<p class="text-xs text-green-300 mb-2">🏆 Best Strategy</p>
<p class="text-xl font-bold text-white">${getCategoryLabel(best.trade_category)}</p>
<p class="text-2xl font-bold text-green-400 mt-2">+${formatCurrency(best.net_pnl)}</p>
<p class="text-xs text-slate-400 mt-1">${best.win_rate}% win rate</p>
</div>
<div class="glass rounded-2xl p-5 border border-red-500/20">
<p class="text-xs text-red-300 mb-2">⚠️ Needs Work</p>
<p class="text-xl font-bold text-white">${getCategoryLabel(worst.trade_category)}</p>
<p class="text-2xl font-bold text-red-400 mt-2">${formatCurrency(worst.net_pnl)}</p>
<p class="text-xs text-slate-400 mt-1">${worst.win_rate}% win rate</p>
</div>
`;
})()}
</div>
` : ''}
</div>
`;
}
function initCategoryCharts() {
const analytics = state.analytics;
if (!analytics.byCategory || analytics.byCategory.length === 0) return;
// P&L Chart
const pnlCanvas = document.getElementById('categoryPnlChart');
if (pnlCanvas) {
const ctx = pnlCanvas.getContext('2d');
if (window.categoryPnlChart && typeof window.categoryPnlChart.destroy === 'function') {
window.categoryPnlChart.destroy();
}
window.categoryPnlChart = new Chart(ctx, {
type: 'bar',
data: {
labels: analytics.byCategory.map(c => getCategoryLabel(c.trade_category)),
datasets: [{
label: 'Net P&L',
data: analytics.byCategory.map(c => parseFloat(c.net_pnl)),
backgroundColor: analytics.byCategory.map(c => parseFloat(c.net_pnl) >= 0 ? '#10b981' : '#ef4444'),
borderRadius: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
callbacks: {
label: (context) => '$' + context.parsed.y.toFixed(2)
}
}
},
scales: {
y: {
grid: { color: 'rgba(139, 92, 246, 0.1)' },
ticks: {
color: '#a78bfa',
callback: (value) => '$' + value.toFixed(0)
}
},
x: {
grid: { display: false },
ticks: { color: '#a78bfa', font: { size: 10 } }
}
}
}
});
}
// Distribution Chart
const distCanvas = document.getElementById('categoryDistChart');
if (distCanvas) {
const ctx = distCanvas.getContext('2d');
if (window.categoryDistChart && typeof window.categoryDistChart.destroy === 'function') {
window.categoryDistChart.destroy();
}
window.categoryDistChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: analytics.byCategory.map(c => getCategoryLabel(c.trade_category)),
datasets: [{
data: analytics.byCategory.map(c => c.total_trades),
backgroundColor: ['#8b5cf6', '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#ec4899']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#a78bfa', font: { size: 10 }, padding: 10 }
},
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12
}
}
}
});
}
}
// ============================================
// PSYCHOLOGY TAB
// ============================================
function renderPsychologyTab() {
const analytics = state.analytics;
return `
<div class="space-y-4">
<!-- Emotion Performance -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">Performance by Emotion</h3>
${analytics.byEmotion && analytics.byEmotion.length > 0 ? `
<div class="space-y-3">
${analytics.byEmotion.map(emo => `
<div class="bg-slate-900/50 rounded-xl p-4 border border-violet-500/20">
<div class="flex items-center justify-between mb-2">
<span class="emotion-badge emotion-${emo.emotion}">
${getEmotionLabel(emo.emotion)}
</span>
<div class="text-right">
<p class="text-sm text-slate-400">${emo.count} trades</p>
<p class="text-lg font-bold ${parseFloat(emo.win_rate) >= 50 ? 'text-green-400' : 'text-red-400'}">
${emo.win_rate}% wins
</p>
</div>
</div>
<div class="w-full bg-slate-800 rounded-full h-2 mt-2">
<div class="h-2 rounded-full ${parseFloat(emo.win_rate) >= 50 ? 'bg-green-500' : 'bg-red-500'}"
style="width: ${emo.win_rate}%"></div>
</div>
</div>
`).join('')}
</div>
` : '<p class="text-slate-400 text-center py-8">No emotion data yet. Start tracking your emotions!</p>'}
</div>
${analytics.byEmotion && analytics.byEmotion.length > 0 ? `
<!-- Emotion Chart -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">Win Rate by Emotion</h3>
<div style="height: 300px;">
<canvas id="emotionChart"></canvas>
</div>
</div>
<!-- Key Insights -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">💡 Key Insights</h3>
${(() => {
const sorted = [...analytics.byEmotion].sort((a, b) => parseFloat(b.win_rate) - parseFloat(a.win_rate));
const best = sorted[0];
const worst = sorted[sorted.length - 1];
return `
<div class="space-y-4">
<div class="bg-green-500/10 rounded-xl p-4 border border-green-500/20">
<p class="text-sm text-green-300 mb-2">✅ Best Trading Emotion</p>
<p class="text-lg font-bold text-white">${getEmotionLabel(best.emotion)}</p>
<p class="text-sm text-slate-400 mt-1">
${best.win_rate}% win rate across ${best.count} trades
</p>
<p class="text-xs text-green-300 mt-2">
💡 Try to achieve this emotional state more often
</p>
</div>
<div class="bg-red-500/10 rounded-xl p-4 border border-red-500/20">
<p class="text-sm text-red-300 mb-2">⚠️ Problematic Emotion</p>
<p class="text-lg font-bold text-white">${getEmotionLabel(worst.emotion)}</p>
<p class="text-sm text-slate-400 mt-1">
${worst.win_rate}% win rate across ${worst.count} trades
</p>
<p class="text-xs text-red-300 mt-2">
💡 Avoid trading when feeling ${worst.emotion} - take a break
</p>
</div>
</div>
`;
})()}
</div>
` : ''}
</div>
`;
}
function initPsychologyCharts() {
const analytics = state.analytics;
if (!analytics.byEmotion || analytics.byEmotion.length === 0) return;
const canvas = document.getElementById('emotionChart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (window.emotionChart && typeof window.emotionChart.destroy === 'function') {
window.emotionChart.destroy();
}
window.emotionChart = new Chart(ctx, {
type: 'radar',
data: {
labels: analytics.byEmotion.map(e => getEmotionLabel(e.emotion)),
datasets: [{
label: 'Win Rate %',
data: analytics.byEmotion.map(e => parseFloat(e.win_rate || 0)),
backgroundColor: 'rgba(139, 92, 246, 0.2)',
borderColor: '#8b5cf6',
pointBackgroundColor: '#8b5cf6',
pointBorderColor: '#fff',
pointHoverBackgroundColor: '#fff',
pointHoverBorderColor: '#8b5cf6'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
callbacks: {
label: (context) => context.parsed.r.toFixed(1) + '% win rate'
}
}
},
scales: {
r: {
beginAtZero: true,
max: 100,
ticks: {
color: '#a78bfa',
backdropColor: 'transparent',
callback: (value) => value + '%'
},
grid: { color: 'rgba(139, 92, 246, 0.2)' },
pointLabels: { color: '#a78bfa', font: { size: 11 } }
}
}
}
});
}
// ============================================
// TIMING TAB
// ============================================
function renderTimingTab() {
const analytics = state.analytics;
return `
<div class="space-y-4">
<!-- Trading Hours -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">Performance by Hour</h3>
${analytics.byHour && analytics.byHour.length > 0 ? `
<div class="space-y-2">
${analytics.byHour.map(hour => `
<div class="flex items-center gap-3">
<span class="text-sm text-slate-400 w-16">
${String(hour.hour).padStart(2, '0')}:00
</span>
<div class="flex-1 bg-slate-900/50 rounded-lg p-3 border border-violet-500/20">
<div class="flex items-center justify-between mb-1">
<span class="text-xs text-slate-400">${hour.trades} trades</span>
<span class="text-sm font-semibold ${parseFloat(hour.win_rate || 0) >= 50 ? 'text-green-400' : 'text-red-400'}">
${hour.win_rate || 0}% wins
</span>
</div>
<div class="w-full bg-slate-800 rounded-full h-1.5">
<div class="h-1.5 rounded-full ${parseFloat(hour.win_rate || 0) >= 50 ? 'bg-green-500' : 'bg-red-500'}"
style="width: ${hour.win_rate || 0}%"></div>
</div>
</div>
</div>
`).join('')}
</div>
` : '<p class="text-slate-400 text-center py-8">Not enough data yet</p>'}
</div>
${analytics.byHour && analytics.byHour.length > 0 ? `
<!-- Trading Hours Chart -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-lg font-semibold text-white mb-4">Win Rate Throughout the Day</h3>
<div style="height: 250px;">
<canvas id="hourChart"></canvas>
</div>
</div>
<!-- Best/Worst Times -->
${(() => {
const sorted = [...analytics.byHour].sort((a, b) => parseFloat(b.win_rate || 0) - parseFloat(a.win_rate || 0));
const best = sorted[0];
const worst = sorted[sorted.length - 1];
return `
<div class="grid grid-cols-2 gap-4">
<div class="glass rounded-2xl p-5 border border-green-500/20">
<p class="text-xs text-green-300 mb-2">🌟 Best Time</p>
<p class="text-3xl font-bold text-white">${String(best.hour).padStart(2, '0')}:00</p>
<p class="text-sm text-green-400 mt-2">${best.win_rate}% win rate</p>
<p class="text-xs text-slate-400 mt-1">${best.trades} trades</p>
</div>
<div class="glass rounded-2xl p-5 border border-red-500/20">
<p class="text-xs text-red-300 mb-2">⚠️ Worst Time</p>
<p class="text-3xl font-bold text-white">${String(worst.hour).padStart(2, '0')}:00</p>
<p class="text-sm text-red-400 mt-2">${worst.win_rate}% win rate</p>
<p class="text-xs text-slate-400 mt-1">${worst.trades} trades</p>
</div>
</div>
`;
})()}
<!-- Recommendations -->
<div class="glass rounded-3xl p-6 border border-blue-500/20">
<h3 class="text-lg font-semibold text-white mb-3">📊 Recommendations</h3>
<div class="space-y-2 text-sm">
${(() => {
const sorted = [...analytics.byHour].sort((a, b) => parseFloat(b.win_rate || 0) - parseFloat(a.win_rate || 0));
const goodHours = sorted.slice(0, 3).map(h => String(h.hour).padStart(2, '0') + ':00').join(', ');
const badHours = sorted.slice(-3).reverse().map(h => String(h.hour).padStart(2, '0') + ':00').join(', ');
return `
<p class="text-green-400">✅ Focus on trading during: ${goodHours}</p>
<p class="text-red-400">⚠️ Be cautious during: ${badHours}</p>
<p class="text-slate-400 mt-3">💡 Consider your energy levels and market conditions at different times</p>
`;
})()}
</div>
</div>
` : ''}
</div>
`;
}
function initTimingCharts() {
const analytics = state.analytics;
if (!analytics.byHour || analytics.byHour.length === 0) return;
const canvas = document.getElementById('hourChart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (window.hourChart && typeof window.hourChart.destroy === 'function') {
window.hourChart.destroy();
}
// Sort by hour
const sorted = [...analytics.byHour].sort((a, b) => a.hour - b.hour);
window.hourChart = new Chart(ctx, {
type: 'line',
data: {
labels: sorted.map(h => String(h.hour).padStart(2, '0') + ':00'),
datasets: [{
label: 'Win Rate %',
data: sorted.map(h => parseFloat(h.win_rate || 0)),
borderColor: '#8b5cf6',
backgroundColor: 'rgba(139, 92, 246, 0.1)',
tension: 0.4,
fill: true,
pointBackgroundColor: '#8b5cf6',
pointBorderColor: '#fff',
pointBorderWidth: 2,
pointRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(0, 0, 0, 0.8)',
padding: 12,
callbacks: {
label: (context) => context.parsed.y.toFixed(1) + '% win rate'
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
grid: { color: 'rgba(139, 92, 246, 0.1)' },
ticks: {
color: '#a78bfa',
callback: (value) => value + '%'
}
},
x: {
grid: { display: false },
ticks: { color: '#a78bfa', font: { size: 10 } }
}
}
}
});
}
// ============================================
// REPORTS TAB — TraderVue-style detailed analytics
// ============================================
function renderReportsTab() {
const trades = state.trades || [];
const closed = trades.filter(t => t.status === 'win' || t.status === 'loss');
if (closed.length === 0) {
return `
<div class="glass rounded-3xl p-12 text-center border border-violet-500/20">
<p class="text-lg text-white mb-2">No Closed Trades Yet</p>
<p class="text-sm text-slate-400">Close some trades to see your performance reports</p>
</div>
`;
}
// Attach computed pnl to each trade
const withPnl = closed.map(t => {
const pnl = (t.actual_pnl !== undefined && t.actual_pnl !== null)
? parseFloat(t.actual_pnl)
: t.status === 'win'
? parseFloat(t.profit_amount || 0)
: -parseFloat(t.loss_amount || 0);
return { ...t, _pnl: pnl };
});
// Summary stats
const netPnl = withPnl.reduce((s, t) => s + t._pnl, 0);
const winTrades = withPnl.filter(t => t._pnl > 0);
const lossTrades = withPnl.filter(t => t._pnl < 0);
const totalWins = winTrades.reduce((s, t) => s + t._pnl, 0);
const totalLosses = Math.abs(lossTrades.reduce((s, t) => s + t._pnl, 0));
const profitFactor = totalLosses > 0 ? (totalWins / totalLosses).toFixed(2) : totalWins > 0 ? '∞' : '0.00';
const avgWin = winTrades.length > 0 ? totalWins / winTrades.length : 0;
const avgLoss = lossTrades.length > 0 ? totalLosses / lossTrades.length : 0;
const winRate = withPnl.length > 0 ? Math.round(winTrades.length / withPnl.length * 100) : 0;
const expectancy = withPnl.length > 0 ? netPnl / withPnl.length : 0;
// Daily P&L
const dailyMap = {};
withPnl.forEach(t => {
const day = t.created_at ? String(t.created_at).substring(0, 10) : '—';
dailyMap[day] = (dailyMap[day] || 0) + t._pnl;
});
const dailyPnl = Object.entries(dailyMap).filter(([d]) => d !== '—').sort((a, b) => a[0].localeCompare(b[0]));
// Cumulative P&L
let cumSum = 0;
const cumulativePnl = dailyPnl.map(([day, pnl]) => { cumSum += pnl; return [day, +cumSum.toFixed(2)]; });
// Day-of-week stats
const dowNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const dowStats = Array.from({length: 7}, () => ({ t: 0, w: 0 }));
withPnl.forEach(t => {
if (!t.created_at) return;
const d = new Date(t.created_at).getDay();
dowStats[d].t++;
if (t._pnl > 0) dowStats[d].w++;
});
const dowWinRate = dowStats.map((s, i) => ({ name: dowNames[i], wr: s.t > 0 ? Math.round(s.w / s.t * 100) : null, trades: s.t }));
// Best / worst trades
const sortedByPnl = [...withPnl].sort((a, b) => b._pnl - a._pnl);
const bestTrades = sortedByPnl.slice(0, 5);
const worstTrades = [...sortedByPnl].reverse().slice(0, 5);
const pnlColor = (v) => v >= 0 ? 'text-green-400' : 'text-red-400';
const fmtPnl = (v) => `${v >= 0 ? '+' : ''}$${Math.abs(v).toFixed(2)}`;
const fmtDate = (d) => d ? new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' }) : '—';
return `
<div class="space-y-5">
<!-- ── Summary Stats ── -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
${[
['Net P&L', fmtPnl(netPnl), netPnl >= 0 ? 'text-green-400' : 'text-red-400'],
['Profit Factor', profitFactor, parseFloat(profitFactor) >= 1 ? 'text-green-400' : 'text-red-400'],
['Win Rate', winRate + '%', winRate >= 50 ? 'text-green-400' : 'text-red-400'],
['Avg Win', '+$' + avgWin.toFixed(2), 'text-green-400'],
['Avg Loss', '-$' + avgLoss.toFixed(2),'text-red-400'],
].map(([label, val, cls]) => `
<div class="glass rounded-2xl p-4 border border-violet-500/20 text-center">
<p class="text-xs text-slate-500 mb-1">${label}</p>
<p class="text-xl font-black ${cls}">${val}</p>
</div>
`).join('')}
</div>
<!-- ── Additional Stats Row ── -->
<div class="grid grid-cols-3 gap-3">
${[
['Closed Trades', closed.length, 'text-white'],
['Avg Trade', fmtPnl(expectancy), pnlColor(expectancy)],
['Max Drawdown', '-$' + Math.abs(Math.min(0, ...cumulativePnl.map(([,v])=>v))).toFixed(2), 'text-red-400'],
].map(([label, val, cls]) => `
<div class="glass rounded-2xl p-4 border border-violet-500/20 text-center">
<p class="text-xs text-slate-500 mb-1">${label}</p>
<p class="text-lg font-bold ${cls}">${val}</p>
</div>
`).join('')}
</div>
<!-- ── Daily P&L Bar Chart ── -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-base font-semibold text-white mb-4">Daily P&L</h3>
<div style="height: 240px;">
<canvas id="dailyPnlChart"></canvas>
</div>
</div>
<!-- ── Cumulative P&L Line Chart ── -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-base font-semibold text-white mb-4">Cumulative P&L</h3>
<div style="height: 240px;">
<canvas id="cumulativePnlChart"></canvas>
</div>
</div>
<!-- ── Win Rate by Day of Week ── -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<h3 class="text-base font-semibold text-white mb-4">Win Rate by Day of Week</h3>
<div style="height: 220px;">
<canvas id="dowWinRateChart"></canvas>
</div>
</div>
<!-- ── Best & Worst Trades ── -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Best -->
<div class="glass rounded-3xl p-5 border border-green-500/20">
<h3 class="text-sm font-semibold text-green-300 mb-3">🏆 Best Trades</h3>
<table class="w-full text-xs">
<thead>
<tr class="text-slate-500 border-b border-slate-700/50">
<th class="pb-2 text-left font-medium">Symbol</th>
<th class="pb-2 text-left font-medium">Date</th>
<th class="pb-2 text-right font-medium">P&L</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/60">
${bestTrades.map(t => `
<tr class="hover:bg-green-500/5 cursor-pointer transition"
onclick="openTradeDetailModal(window._tradeRegistry[${t.id}])">
<td class="py-2 font-bold text-white">${t.coin || '—'}</td>
<td class="py-2 text-slate-400">${fmtDate(t.created_at)}</td>
<td class="py-2 text-right font-bold text-green-400">${fmtPnl(t._pnl)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<!-- Worst -->
<div class="glass rounded-3xl p-5 border border-red-500/20">
<h3 class="text-sm font-semibold text-red-300 mb-3">⚠️ Worst Trades</h3>
<table class="w-full text-xs">
<thead>
<tr class="text-slate-500 border-b border-slate-700/50">
<th class="pb-2 text-left font-medium">Symbol</th>
<th class="pb-2 text-left font-medium">Date</th>
<th class="pb-2 text-right font-medium">P&L</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/60">
${worstTrades.map(t => `
<tr class="hover:bg-red-500/5 cursor-pointer transition"
onclick="openTradeDetailModal(window._tradeRegistry[${t.id}])">
<td class="py-2 font-bold text-white">${t.coin || '—'}</td>
<td class="py-2 text-slate-400">${fmtDate(t.created_at)}</td>
<td class="py-2 text-right font-bold text-red-400">${fmtPnl(t._pnl)}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
</div>
`;
}
function initReportsCharts() {
const trades = state.trades || [];
const closed = trades.filter(t => t.status === 'win' || t.status === 'loss');
if (closed.length === 0) return;
const withPnl = closed.map(t => {
const pnl = (t.actual_pnl !== undefined && t.actual_pnl !== null)
? parseFloat(t.actual_pnl)
: t.status === 'win'
? parseFloat(t.profit_amount || 0)
: -parseFloat(t.loss_amount || 0);
return { ...t, _pnl: pnl };
});
// Daily P&L
const dailyMap = {};
withPnl.forEach(t => {
const day = t.created_at ? String(t.created_at).substring(0, 10) : null;
if (day) dailyMap[day] = (dailyMap[day] || 0) + t._pnl;
});
const dailyEntries = Object.entries(dailyMap).sort((a, b) => a[0].localeCompare(b[0]));
const dailyLabels = dailyEntries.map(([d]) => {
const dt = new Date(d + 'T00:00:00');
return dt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
});
const dailyValues = dailyEntries.map(([, v]) => +v.toFixed(2));
// Cumulative P&L
let cum = 0;
const cumValues = dailyValues.map(v => +(cum += v).toFixed(2));
// Day-of-week win rates
const dowNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const dowStats = Array.from({length: 7}, () => ({ t: 0, w: 0 }));
withPnl.forEach(t => {
if (!t.created_at) return;
const d = new Date(t.created_at).getDay();
dowStats[d].t++;
if (t._pnl > 0) dowStats[d].w++;
});
const dowWr = dowStats.map(s => s.t > 0 ? Math.round(s.w / s.t * 100) : null);
const chartDefaults = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { backgroundColor: 'rgba(0,0,0,0.85)', padding: 12 }
},
scales: {
y: { grid: { color: 'rgba(139,92,246,0.1)' }, ticks: { color: '#a78bfa', callback: v => '$' + v.toFixed(0) } },
x: { grid: { display: false }, ticks: { color: '#a78bfa', font: { size: 10 }, maxRotation: 45 } }
}
};
// Daily P&L bar chart
const dailyCanvas = document.getElementById('dailyPnlChart');
if (dailyCanvas) {
if (window._dailyPnlChart && window._dailyPnlChart.destroy) window._dailyPnlChart.destroy();
window._dailyPnlChart = new Chart(dailyCanvas.getContext('2d'), {
type: 'bar',
data: {
labels: dailyLabels,
datasets: [{
label: 'Daily P&L',
data: dailyValues,
backgroundColor: dailyValues.map(v => v >= 0 ? 'rgba(16,185,129,0.75)' : 'rgba(239,68,68,0.75)'),
borderRadius: 6
}]
},
options: {
...chartDefaults,
plugins: {
...chartDefaults.plugins,
tooltip: { ...chartDefaults.plugins.tooltip, callbacks: { label: ctx => (ctx.parsed.y >= 0 ? '+' : '') + '$' + ctx.parsed.y.toFixed(2) } }
}
}
});
}
// Cumulative P&L line chart
const cumCanvas = document.getElementById('cumulativePnlChart');
if (cumCanvas) {
if (window._cumulativePnlChart && window._cumulativePnlChart.destroy) window._cumulativePnlChart.destroy();
const finalVal = cumValues[cumValues.length - 1] || 0;
const lineColor = finalVal >= 0 ? '#10b981' : '#ef4444';
window._cumulativePnlChart = new Chart(cumCanvas.getContext('2d'), {
type: 'line',
data: {
labels: dailyLabels,
datasets: [{
label: 'Cumulative P&L',
data: cumValues,
borderColor: lineColor,
backgroundColor: finalVal >= 0 ? 'rgba(16,185,129,0.1)' : 'rgba(239,68,68,0.1)',
tension: 0.3,
fill: true,
pointBackgroundColor: lineColor,
pointRadius: cumValues.length > 30 ? 2 : 4,
pointBorderColor: '#fff',
pointBorderWidth: 1
}]
},
options: {
...chartDefaults,
plugins: {
...chartDefaults.plugins,
tooltip: { ...chartDefaults.plugins.tooltip, callbacks: { label: ctx => (ctx.parsed.y >= 0 ? '+' : '') + '$' + ctx.parsed.y.toFixed(2) } }
}
}
});
}
// Day-of-week win rate bar chart
const dowCanvas = document.getElementById('dowWinRateChart');
if (dowCanvas) {
if (window._dowWinRateChart && window._dowWinRateChart.destroy) window._dowWinRateChart.destroy();
window._dowWinRateChart = new Chart(dowCanvas.getContext('2d'), {
type: 'bar',
data: {
labels: dowNames,
datasets: [{
label: 'Win Rate %',
data: dowWr,
backgroundColor: dowWr.map(v => v === null ? 'rgba(100,116,139,0.3)' : v >= 50 ? 'rgba(16,185,129,0.75)' : 'rgba(239,68,68,0.75)'),
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(0,0,0,0.85)',
padding: 12,
callbacks: {
label: ctx => ctx.parsed.y !== null ? ctx.parsed.y + '% win rate' : 'No trades'
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
grid: { color: 'rgba(139,92,246,0.1)' },
ticks: { color: '#a78bfa', callback: v => v + '%' }
},
x: { grid: { display: false }, ticks: { color: '#a78bfa' } }
}
}
});
}
}
// Initialize Analytics Charts
function initAnalyticsCharts() {
// Set default tab on load
switchAnalyticsTab(currentAnalyticsTab || 'overview');
}
// Make functions global
window.renderAnalytics = renderAnalytics;
window.initAnalyticsCharts = initAnalyticsCharts;
window.switchAnalyticsTab = switchAnalyticsTab;