/** * Performance Analytics Dashboard Component * Tracks trading performance, win rates, and P&L */ class PerformanceDashboard { constructor(containerId, options = {}) { this.container = document.getElementById(containerId); this.options = { currency: 'USD', riskPerTrade: 0.5, // percentage colors: { profit: '#3fb950', loss: '#f85149', neutral: '#8b949e', breakeven: '#d29922', ...options.colors }, ...options }; this.stats = { totalTrades: 0, wins: 0, losses: 0, breakevens: 0, totalPnL: 0, dailyPnL: 0, weeklyPnL: 0, avgWin: 0, avgLoss: 0, avgRR: 0, expectancy: 0, maxDrawdown: 0, currentDrawdown: 0, bestTrade: null, worstTrade: null, byPattern: {} }; this.trades = []; this._init(); } _init() { this.container.innerHTML = `
Daily P&L
$0.00
0%
Win Rate
0%
0W / 0L
Avg R:R
0.0
Exp: 0.00R
Drawdown
0%
Max: 0%
Equity Curve
Performance by Pattern
No pattern data yet
Recent Trades
Time Symbol Side Pattern Entry Exit P&L R
No trades recorded
Risk Status
Daily Loss Limit:
0% used
Trades Today: 0 / 8
✓ Trading Allowed
`; this._cacheElements(); this._initChart(); this._initControls(); } _cacheElements() { this.els = { dailyPnL: document.getElementById('perfDailyPnL'), dailyPct: document.getElementById('perfDailyPct'), winRate: document.getElementById('perfWinRate'), winLoss: document.getElementById('perfWinLoss'), avgRR: document.getElementById('perfAvgRR'), expectancy: document.getElementById('perfExpectancy'), drawdown: document.getElementById('perfDrawdown'), maxDD: document.getElementById('perfMaxDD'), chartCanvas: document.getElementById('perfChartCanvas'), patterns: document.getElementById('perfPatterns'), tradesTbody: document.getElementById('perfTradesTbody'), riskFill: document.getElementById('perfRiskFill'), riskPct: document.getElementById('perfRiskPct'), tradesToday: document.getElementById('perfTradesToday'), riskStatus: document.getElementById('perfRiskStatus') }; } _initChart() { this.chartCtx = this.els.chartCanvas.getContext('2d'); this.equityCurve = []; this._resizeChart(); window.addEventListener('resize', () => this._resizeChart()); } _resizeChart() { const container = this.els.chartCanvas.parentElement; const rect = container.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; this.els.chartCanvas.width = rect.width * dpr; this.els.chartCanvas.height = rect.height * dpr; this.els.chartCanvas.style.width = rect.width + 'px'; this.els.chartCanvas.style.height = rect.height + 'px'; this.chartCtx.scale(dpr, dpr); this.chartWidth = rect.width; this.chartHeight = rect.height; this._renderChart(); } _initControls() { // Chart range buttons document.querySelectorAll('.perf-chart-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.perf-chart-btn').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); this.chartRange = e.target.dataset.range; this._renderChart(); }); }); // Export button document.getElementById('perfExport').addEventListener('click', () => { this._exportCSV(); }); this.chartRange = 'day'; } /** * Update with new stats */ setStats(stats) { this.stats = { ...this.stats, ...stats }; this._render(); } /** * Add completed trade */ addTrade(trade) { this.trades.unshift({ ...trade, id: trade.id || Date.now(), timestamp: trade.timestamp || Date.now() }); // Update stats this._recalculateStats(); this._render(); } /** * Set full trade history */ setTrades(trades) { this.trades = trades || []; this._recalculateStats(); this._render(); } _recalculateStats() { const today = new Date().toDateString(); const todayTrades = this.trades.filter(t => new Date(t.timestamp).toDateString() === today ); let wins = 0, losses = 0, breakevens = 0; let totalWinPnL = 0, totalLossPnL = 0; let dailyPnL = 0; let equity = 0; let peak = 0; let maxDD = 0; const byPattern = {}; this.trades.forEach(trade => { const pnl = trade.pnl || 0; const rMultiple = trade.rMultiple || 0; if (pnl > 0) { wins++; totalWinPnL += pnl; } else if (pnl < 0) { losses++; totalLossPnL += Math.abs(pnl); } else { breakevens++; } // Equity curve equity += pnl; if (equity > peak) peak = equity; const dd = peak > 0 ? ((peak - equity) / peak) * 100 : 0; if (dd > maxDD) maxDD = dd; // Pattern tracking const pattern = trade.pattern || 'Unknown'; if (!byPattern[pattern]) { byPattern[pattern] = { wins: 0, losses: 0, pnl: 0, trades: 0 }; } byPattern[pattern].trades++; byPattern[pattern].pnl += pnl; if (pnl > 0) byPattern[pattern].wins++; else if (pnl < 0) byPattern[pattern].losses++; }); todayTrades.forEach(t => dailyPnL += (t.pnl || 0)); const totalTrades = wins + losses + breakevens; const winRate = totalTrades > 0 ? (wins / totalTrades) * 100 : 0; const avgWin = wins > 0 ? totalWinPnL / wins : 0; const avgLoss = losses > 0 ? totalLossPnL / losses : 0; const avgRR = avgLoss > 0 ? avgWin / avgLoss : 0; // Expectancy = (Win% × Avg Win) - (Loss% × Avg Loss) const expectancy = totalTrades > 0 ? ((winRate / 100) * avgWin) - (((100 - winRate) / 100) * avgLoss) : 0; const currentDD = peak > 0 ? ((peak - equity) / peak) * 100 : 0; this.stats = { totalTrades, wins, losses, breakevens, totalPnL: equity, dailyPnL, avgWin, avgLoss, avgRR, expectancy, maxDrawdown: maxDD, currentDrawdown: currentDD, byPattern, tradesToday: todayTrades.length }; // Build equity curve this._buildEquityCurve(); } _buildEquityCurve() { this.equityCurve = []; let equity = 0; const sortedTrades = [...this.trades].sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0) ); sortedTrades.forEach(trade => { equity += (trade.pnl || 0); this.equityCurve.push({ time: trade.timestamp, value: equity }); }); } _render() { this._renderSummary(); this._renderChart(); this._renderPatterns(); this._renderTrades(); this._renderRisk(); } _renderSummary() { const { dailyPnL, wins, losses, avgRR, expectancy, currentDrawdown, maxDrawdown } = this.stats; const totalTrades = wins + losses + (this.stats.breakevens || 0); const winRate = totalTrades > 0 ? (wins / totalTrades) * 100 : 0; // Daily P&L this.els.dailyPnL.textContent = this._formatCurrency(dailyPnL); this.els.dailyPnL.className = 'perf-card-value ' + (dailyPnL > 0 ? 'perf-profit' : dailyPnL < 0 ? 'perf-loss' : ''); const dailyPct = 2; // Assuming 2% daily limit this.els.dailyPct.textContent = `${((dailyPnL / 10000) * 100).toFixed(2)}%`; // Relative to account // Win Rate this.els.winRate.textContent = winRate.toFixed(1) + '%'; this.els.winRate.className = 'perf-card-value ' + (winRate >= 50 ? 'perf-profit' : 'perf-loss'); this.els.winLoss.textContent = `${wins}W / ${losses}L`; // Avg R:R this.els.avgRR.textContent = avgRR.toFixed(2); this.els.expectancy.textContent = `Exp: ${expectancy >= 0 ? '+' : ''}${expectancy.toFixed(2)}R`; // Drawdown this.els.drawdown.textContent = currentDrawdown.toFixed(1) + '%'; this.els.drawdown.className = 'perf-card-value perf-dd ' + (currentDrawdown > 5 ? 'perf-loss' : currentDrawdown > 2 ? 'perf-warn' : ''); this.els.maxDD.textContent = `Max: ${maxDrawdown.toFixed(1)}%`; } _renderChart() { const ctx = this.chartCtx; if (!ctx) return; ctx.clearRect(0, 0, this.chartWidth, this.chartHeight); // Filter by range let data = this.equityCurve; const now = Date.now(); switch (this.chartRange) { case 'day': data = data.filter(d => now - d.time < 86400000); break; case 'week': data = data.filter(d => now - d.time < 604800000); break; case 'month': data = data.filter(d => now - d.time < 2592000000); break; } if (data.length < 2) { ctx.fillStyle = '#8b949e'; ctx.font = '12px Consolas'; ctx.textAlign = 'center'; ctx.fillText('Not enough data', this.chartWidth / 2, this.chartHeight / 2); return; } // Calculate scales const values = data.map(d => d.value); const minVal = Math.min(0, ...values); const maxVal = Math.max(0, ...values); const range = maxVal - minVal || 1; const padding = 20; const xScale = (this.chartWidth - padding * 2) / (data.length - 1); const yScale = (this.chartHeight - padding * 2) / range; // Draw zero line const zeroY = this.chartHeight - padding - (0 - minVal) * yScale; ctx.strokeStyle = '#30363d'; ctx.lineWidth = 1; ctx.setLineDash([5, 5]); ctx.beginPath(); ctx.moveTo(padding, zeroY); ctx.lineTo(this.chartWidth - padding, zeroY); ctx.stroke(); ctx.setLineDash([]); // Draw equity line ctx.strokeStyle = data[data.length - 1].value >= 0 ? this.options.colors.profit : this.options.colors.loss; ctx.lineWidth = 2; ctx.beginPath(); data.forEach((point, i) => { const x = padding + i * xScale; const y = this.chartHeight - padding - (point.value - minVal) * yScale; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // Fill area const lastPoint = data[data.length - 1]; const fillColor = lastPoint.value >= 0 ? 'rgba(63, 185, 80, 0.1)' : 'rgba(248, 81, 73, 0.1)'; ctx.fillStyle = fillColor; ctx.lineTo(this.chartWidth - padding, zeroY); ctx.lineTo(padding, zeroY); ctx.closePath(); ctx.fill(); } _renderPatterns() { const { byPattern } = this.stats; const patterns = Object.entries(byPattern); if (patterns.length === 0) { this.els.patterns.innerHTML = '
No pattern data yet
'; return; } const html = patterns.map(([name, data]) => { const winRate = data.trades > 0 ? (data.wins / data.trades) * 100 : 0; const pnlClass = data.pnl > 0 ? 'perf-profit' : data.pnl < 0 ? 'perf-loss' : ''; return `
${name} ${data.trades} trades ${winRate.toFixed(0)}% ${this._formatCurrency(data.pnl)}
`; }).join(''); this.els.patterns.innerHTML = html; } _renderTrades() { const recentTrades = this.trades.slice(0, 20); if (recentTrades.length === 0) { this.els.tradesTbody.innerHTML = ` No trades recorded `; return; } const html = recentTrades.map(trade => { const pnlClass = trade.pnl > 0 ? 'perf-profit' : trade.pnl < 0 ? 'perf-loss' : 'perf-be'; const rMultiple = trade.rMultiple || (trade.pnl / (trade.risk || 1)); return ` ${this._formatTime(trade.timestamp)} ${trade.symbol || '--'} ${trade.side?.toUpperCase() || '--'} ${trade.pattern || '--'} ${this._formatPrice(trade.entry)} ${this._formatPrice(trade.exit)} ${this._formatCurrency(trade.pnl)} ${rMultiple >= 0 ? '+' : ''}${rMultiple.toFixed(1)}R `; }).join(''); this.els.tradesTbody.innerHTML = html; } _renderRisk() { const { dailyPnL, tradesToday } = this.stats; const dailyLimit = 2; // 2% daily loss limit const accountValue = 10000; // Placeholder const maxDailyLoss = accountValue * (dailyLimit / 100); const usedPct = dailyPnL < 0 ? Math.min(100, (Math.abs(dailyPnL) / maxDailyLoss) * 100) : 0; this.els.riskFill.style.width = usedPct + '%'; this.els.riskFill.className = 'perf-risk-fill ' + (usedPct > 80 ? 'perf-risk-danger' : usedPct > 50 ? 'perf-risk-warning' : ''); this.els.riskPct.textContent = usedPct.toFixed(0) + '% used'; this.els.tradesToday.textContent = `${tradesToday || 0} / 8`; // Risk status badge const canTrade = usedPct < 100; this.els.riskStatus.innerHTML = canTrade ? '✓ Trading Allowed' : '✕ Daily Limit Reached'; } _formatCurrency(value) { const sign = value >= 0 ? '+' : ''; return sign + '$' + Math.abs(value).toFixed(2); } _formatPrice(price) { if (!price) return '--'; if (price >= 1000) return price.toFixed(1); if (price >= 1) return price.toFixed(2); return price.toFixed(4); } _formatTime(timestamp) { if (!timestamp) return '--'; return new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }); } _exportCSV() { const headers = ['Time', 'Symbol', 'Side', 'Pattern', 'Entry', 'Exit', 'P&L', 'R Multiple']; const rows = this.trades.map(t => [ new Date(t.timestamp).toISOString(), t.symbol, t.side, t.pattern, t.entry, t.exit, t.pnl, t.rMultiple ]); const csv = [headers, ...rows].map(r => r.join(',')).join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `trades_${new Date().toISOString().split('T')[0]}.csv`; a.click(); URL.revokeObjectURL(url); } destroy() { this.container.innerHTML = ''; } } // Export for use in main app window.PerformanceDashboard = PerformanceDashboard;