Files
OrderFlow-Analysis-Pro/orderflow_system/dashboard/static/performance.js
T
BlackboxAI 0206ef7cbb Initial commit: orderflow analysis system with 5 pattern detectors
Real-time orderflow trading system with absorption, initiative, sweep,
exhaustion, and divergence detection. Features volume profile framing,
state machine trade lifecycle, MT5 + Bybit feeds, FastAPI dashboard,
and Telegram alerts for 30+ instruments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 21:38:25 +03:00

600 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = `
<div class="perf-dashboard">
<!-- Summary Cards Row -->
<div class="perf-summary">
<div class="perf-card perf-pnl-card">
<div class="perf-card-label">Daily P&L</div>
<div class="perf-card-value" id="perfDailyPnL">$0.00</div>
<div class="perf-card-sub" id="perfDailyPct">0%</div>
</div>
<div class="perf-card">
<div class="perf-card-label">Win Rate</div>
<div class="perf-card-value" id="perfWinRate">0%</div>
<div class="perf-card-sub" id="perfWinLoss">0W / 0L</div>
</div>
<div class="perf-card">
<div class="perf-card-label">Avg R:R</div>
<div class="perf-card-value" id="perfAvgRR">0.0</div>
<div class="perf-card-sub" id="perfExpectancy">Exp: 0.00R</div>
</div>
<div class="perf-card">
<div class="perf-card-label">Drawdown</div>
<div class="perf-card-value perf-dd" id="perfDrawdown">0%</div>
<div class="perf-card-sub" id="perfMaxDD">Max: 0%</div>
</div>
</div>
<!-- P&L Chart -->
<div class="perf-chart-section">
<div class="perf-section-header">
<span class="perf-section-title">Equity Curve</span>
<div class="perf-chart-controls">
<button class="perf-chart-btn active" data-range="day">1D</button>
<button class="perf-chart-btn" data-range="week">1W</button>
<button class="perf-chart-btn" data-range="month">1M</button>
<button class="perf-chart-btn" data-range="all">All</button>
</div>
</div>
<div class="perf-chart" id="perfChart">
<canvas id="perfChartCanvas"></canvas>
</div>
</div>
<!-- Pattern Performance -->
<div class="perf-patterns-section">
<div class="perf-section-header">
<span class="perf-section-title">Performance by Pattern</span>
</div>
<div class="perf-patterns" id="perfPatterns">
<div class="perf-pattern-empty">No pattern data yet</div>
</div>
</div>
<!-- Recent Trades -->
<div class="perf-trades-section">
<div class="perf-section-header">
<span class="perf-section-title">Recent Trades</span>
<button class="perf-export-btn" id="perfExport" title="Export CSV">⬇ Export</button>
</div>
<div class="perf-trades-table" id="perfTrades">
<table class="perf-table">
<thead>
<tr>
<th>Time</th>
<th>Symbol</th>
<th>Side</th>
<th>Pattern</th>
<th>Entry</th>
<th>Exit</th>
<th>P&L</th>
<th>R</th>
</tr>
</thead>
<tbody id="perfTradesTbody">
<tr class="perf-empty-row">
<td colspan="8">No trades recorded</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Risk Status -->
<div class="perf-risk-section">
<div class="perf-risk-header">
<span class="perf-section-title">Risk Status</span>
</div>
<div class="perf-risk-content">
<div class="perf-risk-item">
<span class="perf-risk-label">Daily Loss Limit:</span>
<div class="perf-risk-bar">
<div class="perf-risk-fill" id="perfRiskFill"></div>
</div>
<span class="perf-risk-value" id="perfRiskPct">0% used</span>
</div>
<div class="perf-risk-item">
<span class="perf-risk-label">Trades Today:</span>
<span class="perf-risk-value" id="perfTradesToday">0 / 8</span>
</div>
<div class="perf-risk-status" id="perfRiskStatus">
<span class="perf-risk-badge perf-risk-ok">✓ Trading Allowed</span>
</div>
</div>
</div>
</div>
`;
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 = '<div class="perf-pattern-empty">No pattern data yet</div>';
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 `
<div class="perf-pattern-row">
<span class="perf-pattern-name">${name}</span>
<span class="perf-pattern-trades">${data.trades} trades</span>
<span class="perf-pattern-winrate">${winRate.toFixed(0)}%</span>
<span class="perf-pattern-pnl ${pnlClass}">${this._formatCurrency(data.pnl)}</span>
</div>
`;
}).join('');
this.els.patterns.innerHTML = html;
}
_renderTrades() {
const recentTrades = this.trades.slice(0, 20);
if (recentTrades.length === 0) {
this.els.tradesTbody.innerHTML = `
<tr class="perf-empty-row">
<td colspan="8">No trades recorded</td>
</tr>
`;
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 `
<tr class="perf-trade-row ${pnlClass}">
<td>${this._formatTime(trade.timestamp)}</td>
<td>${trade.symbol || '--'}</td>
<td class="${trade.side === 'long' ? 'perf-profit' : 'perf-loss'}">${trade.side?.toUpperCase() || '--'}</td>
<td>${trade.pattern || '--'}</td>
<td>${this._formatPrice(trade.entry)}</td>
<td>${this._formatPrice(trade.exit)}</td>
<td class="${pnlClass}">${this._formatCurrency(trade.pnl)}</td>
<td>${rMultiple >= 0 ? '+' : ''}${rMultiple.toFixed(1)}R</td>
</tr>
`;
}).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
? '<span class="perf-risk-badge perf-risk-ok">✓ Trading Allowed</span>'
: '<span class="perf-risk-badge perf-risk-stop">✕ Daily Limit Reached</span>';
}
_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;