Refine dashboard UI/UX with storyboard-aligned instrument drill-down
- Introduced card-based layout matching storyboard visuals - Added tracked instrument input (symbol + type) with per-instrument cards - Implemented intuitive drill-down: dashboard metrics switch to selected instrument - Displayed P&L, drawdown, and win rate per instrument using static mock data - Updated charts and recent performance table to reflect selected instrument - Prepared for future integration with real Moomoo data layer
This commit is contained in:
@@ -6,173 +6,330 @@
|
||||
<title>Quantitative Trading Platform - Dashboard</title>
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Bootstrap Icons -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
:root {
|
||||
--bg-dark: #060913;
|
||||
--bg-elevated: #0c1220;
|
||||
--bg-card: #111827;
|
||||
--border-subtle: #1f2937;
|
||||
--text-main: #e5e7eb;
|
||||
--text-muted: #9ca3af;
|
||||
--accent-green: #22c55e;
|
||||
--accent-red: #ef4444;
|
||||
--accent-blue: #3b82f6;
|
||||
--accent-amber: #f59e0b;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-dark);
|
||||
color: var(--text-main);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background: linear-gradient(90deg, #020617, #0f172a);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.metric-card {
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s;
|
||||
|
||||
.page-header {
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.metric-card:hover {
|
||||
transform: translateY(-5px);
|
||||
|
||||
.metric-card, .instrument-card, .panel-card {
|
||||
background: radial-gradient(circle at top left, rgba(59,130,246,0.18), transparent 55%),
|
||||
var(--bg-card);
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
box-shadow: 0 18px 35px rgba(0,0,0,0.55);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.metric-card:hover, .instrument-card:hover, .panel-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.7);
|
||||
border-color: rgba(59,130,246,0.7);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 2.5rem;
|
||||
font-size: 2.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.metric-positive {
|
||||
color: #198754;
|
||||
|
||||
.metric-label {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.metric-negative {
|
||||
color: #dc3545;
|
||||
|
||||
.metric-positive { color: var(--accent-green); }
|
||||
.metric-negative { color: var(--accent-red); }
|
||||
.metric-neutral { color: var(--text-muted); }
|
||||
|
||||
.instrument-input-card {
|
||||
background: var(--bg-elevated);
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
padding: 1.25rem 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.metric-neutral {
|
||||
color: #6c757d;
|
||||
|
||||
.instrument-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.instrument-card.active {
|
||||
border-color: var(--accent-blue);
|
||||
box-shadow: 0 0 0 1px rgba(59,130,246,0.5);
|
||||
}
|
||||
|
||||
.instrument-symbol {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.instrument-type-badge {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.badge-soft {
|
||||
border-radius: 999px;
|
||||
background: rgba(148,163,184,0.1);
|
||||
color: var(--text-muted);
|
||||
padding: 0.35rem 0.7rem;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.table-dark.table-hover tbody tr:hover {
|
||||
background-color: rgba(31,41,55,0.95);
|
||||
}
|
||||
|
||||
.subtle-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.kpi-chip {
|
||||
border-radius: 999px;
|
||||
background: rgba(15,23,42,0.9);
|
||||
border: 1px solid rgba(148,163,184,0.35);
|
||||
padding: 0.35rem 0.7rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.kpi-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.dot-green { background: var(--accent-green); }
|
||||
.dot-red { background: var(--accent-red); }
|
||||
.dot-amber { background: var(--accent-amber); }
|
||||
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.8rem;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
📈 Quantitative Trading Platform
|
||||
<i class="bi bi-graph-up-arrow me-2 text-primary"></i>
|
||||
Quantitative Trading Platform
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="/">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Strategies</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Risk Management</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Trade Journal</a>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link active" href="/">Dashboard</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#">Strategies</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#">Risk Management</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#">Trade Journal</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<!-- Page Title -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h1 class="h2">Trading Dashboard</h1>
|
||||
<p class="text-muted">Real-time metrics and performance monitoring</p>
|
||||
<!-- Header & Instrument Selector -->
|
||||
<div class="row page-header align-items-center">
|
||||
<div class="col-md-6">
|
||||
<h1 class="h3 mb-1">Trading Dashboard</h1>
|
||||
<p class="text-muted mb-0">Card-based view of P&L, drawdown, and win rate with per-instrument drill-down</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-outline-primary" onclick="refreshMetrics()">
|
||||
🔄 Refresh
|
||||
<div class="col-md-6 text-md-end mt-3 mt-md-0">
|
||||
<span class="badge-soft me-2"><i class="bi bi-lightbulb me-1"></i> Storyboard-aligned UI</span>
|
||||
<span class="badge-soft me-2"><i class="bi bi-shield-check me-1"></i> Risk-aware metrics</span>
|
||||
<button class="btn btn-outline-light btn-sm" id="refresh-btn">
|
||||
<i class="bi bi-arrow-repeat me-1"></i> Simulate Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics Row -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card metric-card">
|
||||
<!-- Instrument Input & List (Drill-down layer) -->
|
||||
<div class="row mt-3 mb-3">
|
||||
<div class="col-lg-4 mb-3 mb-lg-0">
|
||||
<div class="instrument-input-card">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<div class="subtle-label">Tracked Instruments</div>
|
||||
<small class="text-muted">Add symbols to monitor and click a card to drill into its metrics.</small>
|
||||
</div>
|
||||
</div>
|
||||
<form class="mt-3" id="instrument-form">
|
||||
<div class="row g-2 align-items-center">
|
||||
<div class="col-6">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control form-control-sm bg-dark text-light border-secondary" id="instrument-symbol" placeholder="AAPL, EUR/USD" required>
|
||||
<label for="instrument-symbol" class="text-muted">Symbol</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="form-floating">
|
||||
<select class="form-select form-select-sm bg-dark text-light border-secondary" id="instrument-type">
|
||||
<option value="equity" selected>Equity</option>
|
||||
<option value="forex">Forex</option>
|
||||
</select>
|
||||
<label for="instrument-type" class="text-muted">Type</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-2 d-grid">
|
||||
<button class="btn btn-primary btn-sm" type="submit" title="Add instrument">
|
||||
<i class="bi bi-plus-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-3" id="instrument-list">
|
||||
<!-- Instrument cards populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Metrics Cards (drill-down target) -->
|
||||
<div class="col-lg-8">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="card metric-card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-muted">Total P&L</h5>
|
||||
<div class="metric-value" id="total-pnl">$0.00</div>
|
||||
<p class="card-text">
|
||||
<span id="pnl-trend" class="badge bg-success">+0.0%</span>
|
||||
vs. last month
|
||||
</p>
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<div class="metric-label">Total P&L</div>
|
||||
<div class="metric-value metric-positive" id="total-pnl">$0.00</div>
|
||||
</div>
|
||||
<span class="badge bg-success-subtle text-success" id="pnl-trend">+0.0%</span>
|
||||
</div>
|
||||
<small class="text-muted" id="pnl-caption">vs. initial capital</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card metric-card">
|
||||
<div class="col-md-4">
|
||||
<div class="card metric-card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-muted">Max Drawdown</h5>
|
||||
<div class="metric-label mb-1">Max Drawdown</div>
|
||||
<div class="metric-value metric-negative" id="max-drawdown">$0.00</div>
|
||||
<p class="card-text">
|
||||
<span id="drawdown-percent" class="badge bg-danger">0.0%</span>
|
||||
of portfolio
|
||||
</p>
|
||||
<small class="text-muted">Peak-to-trough • <span id="drawdown-percent">0.0%</span> of equity</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="card metric-card">
|
||||
<div class="col-md-4">
|
||||
<div class="card metric-card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-muted">Win Rate</h5>
|
||||
<div class="metric-label mb-1">Win Rate</div>
|
||||
<div class="metric-value metric-neutral" id="win-rate">0.0%</div>
|
||||
<p class="card-text">
|
||||
<span id="profitable-days" class="badge bg-info">0/30</span>
|
||||
profitable days
|
||||
</p>
|
||||
<small class="text-muted">Profitable days: <span id="profitable-days">0/0</span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts Row -->
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="chart-container">
|
||||
<h4>Cumulative P&L Over Time</h4>
|
||||
<canvas id="pnlChart"></canvas>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-6">
|
||||
<div class="panel-card chart-container h-100">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<div>
|
||||
<div class="subtle-label">Cumulative P&L</div>
|
||||
<small class="text-muted">For selected instrument</small>
|
||||
</div>
|
||||
<span class="kpi-chip">
|
||||
<span class="kpi-dot dot-green"></span>
|
||||
Trend • <span id="kpi-trend">Stable</span>
|
||||
</span>
|
||||
</div>
|
||||
<canvas id="pnlChart" height="150"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="chart-container">
|
||||
<h4>Portfolio Overview</h4>
|
||||
<div class="mt-4">
|
||||
<p><strong>Portfolio Value:</strong> <span id="portfolio-value">$100,000.00</span></p>
|
||||
<p><strong>Peak Value:</strong> <span id="peak-value">$100,000.00</span></p>
|
||||
<p><strong>Trough Value:</strong> <span id="trough-value">$100,000.00</span></p>
|
||||
<p><strong>Last Updated:</strong> <span id="last-updated">Just now</span></p>
|
||||
<div class="col-md-6">
|
||||
<div class="panel-card chart-container h-100">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<div>
|
||||
<div class="subtle-label">Daily P&L Distribution</div>
|
||||
<small class="text-muted">Win/loss ratio</small>
|
||||
</div>
|
||||
<span class="kpi-chip">
|
||||
<span class="kpi-dot dot-amber"></span>
|
||||
Volatility • <span id="kpi-volatility">Normal</span>
|
||||
</span>
|
||||
</div>
|
||||
<canvas id="dailyDistributionChart" height="150"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h5>Daily P&L Distribution</h5>
|
||||
<canvas id="dailyDistributionChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity Table -->
|
||||
<div class="row mt-4">
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<div class="chart-container">
|
||||
<h4>Recent Daily Performance</h4>
|
||||
<table class="table table-hover">
|
||||
<div class="panel-card chart-container">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<div class="subtle-label">Recent Daily Performance</div>
|
||||
<small class="text-muted">Last 10 days for <span id="table-instrument">selected instrument</span></small>
|
||||
</div>
|
||||
<span class="badge-soft"><i class="bi bi-info-circle me-1"></i> Static mock data for UI/UX demo</span>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-dark table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Daily P&L</th>
|
||||
<th>Cumulative P&L</th>
|
||||
<th>Status</th>
|
||||
<th scope="col">Date</th>
|
||||
<th scope="col">Daily P&L</th>
|
||||
<th scope="col">Cumulative P&L</th>
|
||||
<th scope="col">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="daily-table">
|
||||
@@ -183,21 +340,71 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="footer">
|
||||
<p>Quantitative Trading Platform • UI/UX Module • Dashboard v1.0</p>
|
||||
<p class="small">Mock data generated for demonstration. Real trading data will be integrated in later phases.</p>
|
||||
<p>Quantitative Trading Platform • UI/UX Module • Dashboard v2.0 (Storyboard-aligned)</p>
|
||||
<p class="small">Static working prototype using mock per-instrument data. Real trading data and Moomoo API integration will be wired in next phase.</p>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS Bundle with Popper -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Dashboard Script -->
|
||||
<!-- Dashboard Script (static mock, storyboard-driven) -->
|
||||
<script>
|
||||
let pnlChart, distributionChart;
|
||||
|
||||
// Format currency
|
||||
const instrumentData = {
|
||||
"AAPL": {
|
||||
type: "equity",
|
||||
label: "Apple Inc.",
|
||||
metrics: {
|
||||
total_pnl: 1460,
|
||||
max_drawdown: -912,
|
||||
win_rate: 0.72,
|
||||
portfolio_value: 25000,
|
||||
peak: 26000,
|
||||
trough: 24088,
|
||||
kpiTrend: "Uptrend",
|
||||
kpiVolatility: "Moderate"
|
||||
},
|
||||
daily_pnl: generateMockDaily(30, 1460)
|
||||
},
|
||||
"EUR/USD": {
|
||||
type: "forex",
|
||||
label: "EUR vs USD",
|
||||
metrics: {
|
||||
total_pnl: 425,
|
||||
max_drawdown: -600,
|
||||
win_rate: 0.6,
|
||||
portfolio_value: 20000,
|
||||
peak: 20550,
|
||||
trough: 19400,
|
||||
kpiTrend: "Range-bound",
|
||||
kpiVolatility: "Elevated (pre-NFP)"
|
||||
},
|
||||
daily_pnl: generateMockDaily(30, 425, true)
|
||||
},
|
||||
"USD/JPY": {
|
||||
type: "forex",
|
||||
label: "US Dollar vs Yen",
|
||||
metrics: {
|
||||
total_pnl: 310,
|
||||
max_drawdown: -480,
|
||||
win_rate: 0.58,
|
||||
portfolio_value: 15000,
|
||||
peak: 15420,
|
||||
trough: 14540,
|
||||
kpiTrend: "Bullish bias",
|
||||
kpiVolatility: "Normal"
|
||||
},
|
||||
daily_pnl: generateMockDaily(30, 310, true)
|
||||
}
|
||||
};
|
||||
|
||||
let selectedInstrument = "AAPL";
|
||||
|
||||
function formatCurrency(value) {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
@@ -206,47 +413,92 @@
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
// Format percentage
|
||||
function formatPercent(value) {
|
||||
return value.toFixed(1) + '%';
|
||||
return (value * 100).toFixed(1) + '%';
|
||||
}
|
||||
|
||||
// Update UI with metrics
|
||||
function updateUI(metrics) {
|
||||
// Key metrics
|
||||
document.getElementById('total-pnl').textContent = formatCurrency(metrics.total_pnl);
|
||||
document.getElementById('max-drawdown').textContent = formatCurrency(metrics.max_drawdown);
|
||||
document.getElementById('win-rate').textContent = formatPercent(metrics.win_rate);
|
||||
document.getElementById('portfolio-value').textContent = formatCurrency(metrics.portfolio_value);
|
||||
document.getElementById('peak-value').textContent = formatCurrency(metrics.peak);
|
||||
document.getElementById('trough-value').textContent = formatCurrency(metrics.trough);
|
||||
document.getElementById('last-updated').textContent = new Date(metrics.last_updated).toLocaleString();
|
||||
|
||||
// Calculate trend badges
|
||||
const pnlTrend = (metrics.total_pnl / 100000 * 100).toFixed(1);
|
||||
document.getElementById('pnl-trend').textContent = (pnlTrend >= 0 ? '+' : '') + pnlTrend + '%';
|
||||
document.getElementById('pnl-trend').className = pnlTrend >= 0 ? 'badge bg-success' : 'badge bg-danger';
|
||||
|
||||
const drawdownPercent = (metrics.max_drawdown / metrics.portfolio_value * 100).toFixed(1);
|
||||
document.getElementById('drawdown-percent').textContent = drawdownPercent + '%';
|
||||
|
||||
const profitableDays = metrics.daily_pnl.filter(d => d.pnl > 0).length;
|
||||
document.getElementById('profitable-days').textContent = profitableDays + '/30';
|
||||
|
||||
// Update charts
|
||||
updateCharts(metrics);
|
||||
|
||||
// Update table
|
||||
updateTable(metrics.daily_pnl.slice(-10).reverse());
|
||||
function generateMockDaily(days, totalTarget, allowBiggerSwings = false) {
|
||||
const data = [];
|
||||
let cumulative = 0;
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
const base = totalTarget / days;
|
||||
const noise = allowBiggerSwings ? (Math.random() - 0.5) * base * 2 : (Math.random() - 0.5) * base;
|
||||
const pnl = base + noise;
|
||||
cumulative += pnl;
|
||||
data.push({
|
||||
date: date.toISOString().slice(0, 10),
|
||||
pnl: pnl,
|
||||
cumulative: cumulative
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// Update charts
|
||||
function updateCharts(metrics) {
|
||||
const dates = metrics.daily_pnl.map(d => d.date.substring(5)); // MM-DD
|
||||
const cumulative = metrics.daily_pnl.map(d => d.cumulative);
|
||||
const dailyPnl = metrics.daily_pnl.map(d => d.pnl);
|
||||
function buildInstrumentList() {
|
||||
const container = document.getElementById('instrument-list');
|
||||
container.innerHTML = '';
|
||||
Object.entries(instrumentData).forEach(([symbol, data]) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'instrument-card p-2 mb-2';
|
||||
if (symbol === selectedInstrument) card.classList.add('active');
|
||||
card.dataset.symbol = symbol;
|
||||
card.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="instrument-symbol">${symbol}</div>
|
||||
<small class="text-muted">${data.label}</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<span class="badge bg-${data.type === 'equity' ? 'info' : 'warning'} text-dark instrument-type-badge">${data.type}</span>
|
||||
<div class="mt-1 small ${data.metrics.total_pnl >= 0 ? 'text-success' : 'text-danger'}">
|
||||
${data.metrics.total_pnl >= 0 ? '+' : ''}${formatCurrency(data.metrics.total_pnl)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
card.addEventListener('click', () => {
|
||||
selectedInstrument = symbol;
|
||||
buildInstrumentList();
|
||||
updateUI();
|
||||
});
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
const data = instrumentData[selectedInstrument];
|
||||
if (!data) return;
|
||||
|
||||
const m = data.metrics;
|
||||
document.getElementById('total-pnl').textContent = formatCurrency(m.total_pnl);
|
||||
document.getElementById('max-drawdown').textContent = formatCurrency(m.max_drawdown);
|
||||
document.getElementById('win-rate').textContent = formatPercent(m.win_rate);
|
||||
|
||||
const pnlTrendEl = document.getElementById('pnl-trend');
|
||||
const pnlTrendPct = (m.total_pnl / m.portfolio_value * 100).toFixed(1);
|
||||
pnlTrendEl.textContent = `${pnlTrendPct >= 0 ? '+' : ''}${pnlTrendPct}%`;
|
||||
pnlTrendEl.className = `badge ${pnlTrendPct >= 0 ? 'bg-success-subtle text-success' : 'bg-danger-subtle text-danger'}`;
|
||||
|
||||
const drawdownPercent = (Math.abs(m.max_drawdown) / m.portfolio_value * 100).toFixed(1);
|
||||
document.getElementById('drawdown-percent').textContent = `${drawdownPercent}%`;
|
||||
|
||||
const profitableDays = data.daily_pnl.filter(d => d.pnl > 0).length;
|
||||
document.getElementById('profitable-days').textContent = `${profitableDays}/${data.daily_pnl.length}`;
|
||||
|
||||
document.getElementById('kpi-trend').textContent = m.kpiTrend;
|
||||
document.getElementById('kpi-volatility').textContent = m.kpiVolatility;
|
||||
document.getElementById('table-instrument').textContent = `${selectedInstrument} (${data.label})`;
|
||||
|
||||
updateCharts(data.daily_pnl, m);
|
||||
updateTable(data.daily_pnl.slice(-10).reverse());
|
||||
}
|
||||
|
||||
function updateCharts(dailyData, metrics) {
|
||||
const dates = dailyData.map(d => d.date.substring(5));
|
||||
const cumulative = dailyData.map(d => d.cumulative);
|
||||
const dailyPnl = dailyData.map(d => d.pnl);
|
||||
|
||||
// Cumulative P&L Chart
|
||||
if (pnlChart) pnlChart.destroy();
|
||||
const ctx1 = document.getElementById('pnlChart').getContext('2d');
|
||||
pnlChart = new Chart(ctx1, {
|
||||
@@ -256,28 +508,34 @@
|
||||
datasets: [{
|
||||
label: 'Cumulative P&L',
|
||||
data: cumulative,
|
||||
borderColor: '#198754',
|
||||
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
||||
borderColor: '#22c55e',
|
||||
backgroundColor: 'rgba(34,197,94,0.12)',
|
||||
fill: true,
|
||||
tension: 0.4
|
||||
tension: 0.35,
|
||||
pointRadius: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true }
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
ticks: { color: '#6b7280' },
|
||||
grid: { color: 'rgba(31,41,55,0.4)' }
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
color: '#6b7280',
|
||||
callback: value => formatCurrency(value)
|
||||
}
|
||||
},
|
||||
grid: { color: 'rgba(31,41,55,0.4)' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Daily Distribution Chart
|
||||
if (distributionChart) distributionChart.destroy();
|
||||
const ctx2 = document.getElementById('dailyDistributionChart').getContext('2d');
|
||||
const profitable = dailyPnl.filter(p => p > 0).length;
|
||||
@@ -288,19 +546,19 @@
|
||||
labels: ['Profitable Days', 'Unprofitable Days'],
|
||||
datasets: [{
|
||||
data: [profitable, unprofitable],
|
||||
backgroundColor: ['#198754', '#dc3545']
|
||||
backgroundColor: ['#22c55e', '#ef4444'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: 'bottom' }
|
||||
legend: { position: 'bottom', labels: { color: '#9ca3af' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update table
|
||||
function updateTable(dailyData) {
|
||||
const tbody = document.getElementById('daily-table');
|
||||
tbody.innerHTML = '';
|
||||
@@ -313,44 +571,65 @@
|
||||
</td>
|
||||
<td>${formatCurrency(day.cumulative)}</td>
|
||||
<td>
|
||||
<span class="badge ${day.pnl >= 0 ? 'bg-success' : 'bg-danger'}">
|
||||
<span class="badge ${day.pnl >= 0 ? 'bg-success-subtle text-success' : 'bg-danger-subtle text-danger'}">
|
||||
${day.pnl >= 0 ? 'Profit' : 'Loss'}
|
||||
</span>
|
||||
</td>
|
||||
`;
|
||||
</td>`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch metrics from API
|
||||
async function fetchMetrics() {
|
||||
try {
|
||||
const response = await fetch('/api/metrics');
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
const metrics = await response.json();
|
||||
updateUI(metrics);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch metrics:', error);
|
||||
alert('Unable to load metrics. Please check server connection.');
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
buildInstrumentList();
|
||||
updateUI();
|
||||
|
||||
// Refresh metrics
|
||||
function refreshMetrics() {
|
||||
const btn = event?.target || document.querySelector('button[onclick="refreshMetrics()"]');
|
||||
btn.innerHTML = '🔄 Loading...';
|
||||
btn.disabled = true;
|
||||
fetchMetrics().finally(() => {
|
||||
btn.innerHTML = '🔄 Refresh';
|
||||
btn.disabled = false;
|
||||
document.getElementById('instrument-form').addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
const symbolInput = document.getElementById('instrument-symbol');
|
||||
const typeSelect = document.getElementById('instrument-type');
|
||||
const symbol = symbolInput.value.trim().toUpperCase();
|
||||
const type = typeSelect.value;
|
||||
if (!symbol) return;
|
||||
|
||||
if (!instrumentData[symbol]) {
|
||||
instrumentData[symbol] = {
|
||||
type,
|
||||
label: type === 'equity' ? `${symbol} (Equity)` : `${symbol} (FX pair)`,
|
||||
metrics: {
|
||||
total_pnl: 0,
|
||||
max_drawdown: 0,
|
||||
win_rate: 0.5,
|
||||
portfolio_value: 10000,
|
||||
peak: 10000,
|
||||
trough: 10000,
|
||||
kpiTrend: 'New',
|
||||
kpiVolatility: 'Unknown'
|
||||
},
|
||||
daily_pnl: generateMockDaily(30, 0)
|
||||
};
|
||||
}
|
||||
selectedInstrument = symbol;
|
||||
symbolInput.value = '';
|
||||
buildInstrumentList();
|
||||
updateUI();
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on page load
|
||||
document.addEventListener('DOMContentLoaded', fetchMetrics);
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(fetchMetrics, 30000);
|
||||
document.getElementById('refresh-btn').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const btn = e.currentTarget;
|
||||
const original = btn.innerHTML;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Simulating';
|
||||
btn.disabled = true;
|
||||
setTimeout(() => {
|
||||
Object.values(instrumentData).forEach(inst => {
|
||||
inst.daily_pnl = generateMockDaily(30, inst.metrics.total_pnl, inst.type === 'forex');
|
||||
});
|
||||
updateUI();
|
||||
btn.innerHTML = original;
|
||||
btn.disabled = false;
|
||||
}, 600);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user