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,179 +6,337 @@
|
|||||||
<title>Quantitative Trading Platform - Dashboard</title>
|
<title>Quantitative Trading Platform - Dashboard</title>
|
||||||
<!-- Bootstrap 5 CSS -->
|
<!-- Bootstrap 5 CSS -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<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 -->
|
<!-- Chart.js -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<style>
|
<style>
|
||||||
body {
|
:root {
|
||||||
background-color: #f8f9fa;
|
--bg-dark: #060913;
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
--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 {
|
.navbar-brand {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
}
|
}
|
||||||
.metric-card {
|
|
||||||
border-radius: 10px;
|
.page-header {
|
||||||
border: none;
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
padding-bottom: 0.75rem;
|
||||||
transition: transform 0.2s;
|
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 {
|
.metric-value {
|
||||||
font-size: 2.5rem;
|
font-size: 2.4rem;
|
||||||
font-weight: 700;
|
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 {
|
.chart-container {
|
||||||
background: white;
|
padding: 1.25rem 1.5rem;
|
||||||
border-radius: 10px;
|
|
||||||
padding: 20px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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 {
|
.footer {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
padding: 20px;
|
padding: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #6c757d;
|
color: var(--text-muted);
|
||||||
font-size: 0.9rem;
|
font-size: 0.8rem;
|
||||||
|
border-top: 1px solid var(--border-subtle);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="/">
|
<a class="navbar-brand" href="/">
|
||||||
📈 Quantitative Trading Platform
|
<i class="bi bi-graph-up-arrow me-2 text-primary"></i>
|
||||||
|
Quantitative Trading Platform
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav ms-auto">
|
<ul class="navbar-nav ms-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item"><a class="nav-link active" href="/">Dashboard</a></li>
|
||||||
<a class="nav-link active" href="/">Dashboard</a>
|
<li class="nav-item"><a class="nav-link" href="#">Strategies</a></li>
|
||||||
</li>
|
<li class="nav-item"><a class="nav-link" href="#">Risk Management</a></li>
|
||||||
<li class="nav-item">
|
<li class="nav-item"><a class="nav-link" href="#">Trade Journal</a></li>
|
||||||
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container-fluid mt-4">
|
<div class="container-fluid mt-4">
|
||||||
<!-- Page Title -->
|
<!-- Header & Instrument Selector -->
|
||||||
<div class="row mb-4">
|
<div class="row page-header align-items-center">
|
||||||
<div class="col">
|
<div class="col-md-6">
|
||||||
<h1 class="h2">Trading Dashboard</h1>
|
<h1 class="h3 mb-1">Trading Dashboard</h1>
|
||||||
<p class="text-muted">Real-time metrics and performance monitoring</p>
|
<p class="text-muted mb-0">Card-based view of P&L, drawdown, and win rate with per-instrument drill-down</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-md-6 text-md-end mt-3 mt-md-0">
|
||||||
<button class="btn btn-outline-primary" onclick="refreshMetrics()">
|
<span class="badge-soft me-2"><i class="bi bi-lightbulb me-1"></i> Storyboard-aligned UI</span>
|
||||||
🔄 Refresh
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Key Metrics Row -->
|
<!-- Instrument Input & List (Drill-down layer) -->
|
||||||
<div class="row mb-4">
|
<div class="row mt-3 mb-3">
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-lg-4 mb-3 mb-lg-0">
|
||||||
<div class="card metric-card">
|
<div class="instrument-input-card">
|
||||||
<div class="card-body">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<h5 class="card-title text-muted">Total P&L</h5>
|
<div>
|
||||||
<div class="metric-value" id="total-pnl">$0.00</div>
|
<div class="subtle-label">Tracked Instruments</div>
|
||||||
<p class="card-text">
|
<small class="text-muted">Add symbols to monitor and click a card to drill into its metrics.</small>
|
||||||
<span id="pnl-trend" class="badge bg-success">+0.0%</span>
|
</div>
|
||||||
vs. last month
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<form class="mt-3" id="instrument-form">
|
||||||
</div>
|
<div class="row g-2 align-items-center">
|
||||||
<div class="col-md-4 mb-3">
|
<div class="col-6">
|
||||||
<div class="card metric-card">
|
<div class="form-floating">
|
||||||
<div class="card-body">
|
<input type="text" class="form-control form-control-sm bg-dark text-light border-secondary" id="instrument-symbol" placeholder="AAPL, EUR/USD" required>
|
||||||
<h5 class="card-title text-muted">Max Drawdown</h5>
|
<label for="instrument-symbol" class="text-muted">Symbol</label>
|
||||||
<div class="metric-value metric-negative" id="max-drawdown">$0.00</div>
|
</div>
|
||||||
<p class="card-text">
|
</div>
|
||||||
<span id="drawdown-percent" class="badge bg-danger">0.0%</span>
|
<div class="col-4">
|
||||||
of portfolio
|
<div class="form-floating">
|
||||||
</p>
|
<select class="form-select form-select-sm bg-dark text-light border-secondary" id="instrument-type">
|
||||||
</div>
|
<option value="equity" selected>Equity</option>
|
||||||
</div>
|
<option value="forex">Forex</option>
|
||||||
</div>
|
</select>
|
||||||
<div class="col-md-4 mb-3">
|
<label for="instrument-type" class="text-muted">Type</label>
|
||||||
<div class="card metric-card">
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<h5 class="card-title text-muted">Win Rate</h5>
|
<div class="col-2 d-grid">
|
||||||
<div class="metric-value metric-neutral" id="win-rate">0.0%</div>
|
<button class="btn btn-primary btn-sm" type="submit" title="Add instrument">
|
||||||
<p class="card-text">
|
<i class="bi bi-plus-lg"></i>
|
||||||
<span id="profitable-days" class="badge bg-info">0/30</span>
|
</button>
|
||||||
profitable days
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Charts Row -->
|
<div class="mt-3" id="instrument-list">
|
||||||
<div class="row">
|
<!-- Instrument cards populated by JS -->
|
||||||
<div class="col-lg-8">
|
</div>
|
||||||
<div class="chart-container">
|
|
||||||
<h4>Cumulative P&L Over Time</h4>
|
|
||||||
<canvas id="pnlChart"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-4">
|
|
||||||
<div class="chart-container">
|
<!-- Key Metrics Cards (drill-down target) -->
|
||||||
<h4>Portfolio Overview</h4>
|
<div class="col-lg-8">
|
||||||
<div class="mt-4">
|
<div class="row g-3">
|
||||||
<p><strong>Portfolio Value:</strong> <span id="portfolio-value">$100,000.00</span></p>
|
<div class="col-md-4">
|
||||||
<p><strong>Peak Value:</strong> <span id="peak-value">$100,000.00</span></p>
|
<div class="card metric-card h-100">
|
||||||
<p><strong>Trough Value:</strong> <span id="trough-value">$100,000.00</span></p>
|
<div class="card-body">
|
||||||
<p><strong>Last Updated:</strong> <span id="last-updated">Just now</span></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">
|
||||||
|
<div class="card metric-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="metric-label mb-1">Max Drawdown</div>
|
||||||
|
<div class="metric-value metric-negative" id="max-drawdown">$0.00</div>
|
||||||
|
<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">
|
||||||
|
<div class="card metric-card h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="metric-label mb-1">Win Rate</div>
|
||||||
|
<div class="metric-value metric-neutral" id="win-rate">0.0%</div>
|
||||||
|
<small class="text-muted">Profitable days: <span id="profitable-days">0/0</span></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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-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>
|
</div>
|
||||||
<hr>
|
|
||||||
<h5>Daily P&L Distribution</h5>
|
|
||||||
<canvas id="dailyDistributionChart"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Recent Activity Table -->
|
<!-- Recent Activity Table -->
|
||||||
<div class="row mt-4">
|
<div class="row mt-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="chart-container">
|
<div class="panel-card chart-container">
|
||||||
<h4>Recent Daily Performance</h4>
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<table class="table table-hover">
|
<div>
|
||||||
<thead>
|
<div class="subtle-label">Recent Daily Performance</div>
|
||||||
<tr>
|
<small class="text-muted">Last 10 days for <span id="table-instrument">selected instrument</span></small>
|
||||||
<th>Date</th>
|
</div>
|
||||||
<th>Daily P&L</th>
|
<span class="badge-soft"><i class="bi bi-info-circle me-1"></i> Static mock data for UI/UX demo</span>
|
||||||
<th>Cumulative P&L</th>
|
</div>
|
||||||
<th>Status</th>
|
<div class="table-responsive">
|
||||||
</tr>
|
<table class="table table-dark table-hover align-middle mb-0">
|
||||||
</thead>
|
<thead>
|
||||||
<tbody id="daily-table">
|
<tr>
|
||||||
<!-- Filled by JavaScript -->
|
<th scope="col">Date</th>
|
||||||
</tbody>
|
<th scope="col">Daily P&L</th>
|
||||||
</table>
|
<th scope="col">Cumulative P&L</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="daily-table">
|
||||||
|
<!-- Filled by JavaScript -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,18 +344,67 @@
|
|||||||
|
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>Quantitative Trading Platform • UI/UX Module • Dashboard v1.0</p>
|
<p>Quantitative Trading Platform • UI/UX Module • Dashboard v2.0 (Storyboard-aligned)</p>
|
||||||
<p class="small">Mock data generated for demonstration. Real trading data will be integrated in later phases.</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>
|
</div>
|
||||||
|
|
||||||
<!-- Bootstrap JS Bundle with Popper -->
|
<!-- Bootstrap JS Bundle with Popper -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<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>
|
<script>
|
||||||
let pnlChart, distributionChart;
|
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) {
|
function formatCurrency(value) {
|
||||||
return new Intl.NumberFormat('en-US', {
|
return new Intl.NumberFormat('en-US', {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
@@ -205,48 +412,93 @@
|
|||||||
minimumFractionDigits: 2
|
minimumFractionDigits: 2
|
||||||
}).format(value);
|
}).format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format percentage
|
|
||||||
function formatPercent(value) {
|
function formatPercent(value) {
|
||||||
return value.toFixed(1) + '%';
|
return (value * 100).toFixed(1) + '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update UI with metrics
|
function generateMockDaily(days, totalTarget, allowBiggerSwings = false) {
|
||||||
function updateUI(metrics) {
|
const data = [];
|
||||||
// Key metrics
|
let cumulative = 0;
|
||||||
document.getElementById('total-pnl').textContent = formatCurrency(metrics.total_pnl);
|
for (let i = days - 1; i >= 0; i--) {
|
||||||
document.getElementById('max-drawdown').textContent = formatCurrency(metrics.max_drawdown);
|
const date = new Date();
|
||||||
document.getElementById('win-rate').textContent = formatPercent(metrics.win_rate);
|
date.setDate(date.getDate() - i);
|
||||||
document.getElementById('portfolio-value').textContent = formatCurrency(metrics.portfolio_value);
|
const base = totalTarget / days;
|
||||||
document.getElementById('peak-value').textContent = formatCurrency(metrics.peak);
|
const noise = allowBiggerSwings ? (Math.random() - 0.5) * base * 2 : (Math.random() - 0.5) * base;
|
||||||
document.getElementById('trough-value').textContent = formatCurrency(metrics.trough);
|
const pnl = base + noise;
|
||||||
document.getElementById('last-updated').textContent = new Date(metrics.last_updated).toLocaleString();
|
cumulative += pnl;
|
||||||
|
data.push({
|
||||||
// Calculate trend badges
|
date: date.toISOString().slice(0, 10),
|
||||||
const pnlTrend = (metrics.total_pnl / 100000 * 100).toFixed(1);
|
pnl: pnl,
|
||||||
document.getElementById('pnl-trend').textContent = (pnlTrend >= 0 ? '+' : '') + pnlTrend + '%';
|
cumulative: cumulative
|
||||||
document.getElementById('pnl-trend').className = pnlTrend >= 0 ? 'badge bg-success' : 'badge bg-danger';
|
});
|
||||||
|
}
|
||||||
const drawdownPercent = (metrics.max_drawdown / metrics.portfolio_value * 100).toFixed(1);
|
return data;
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update charts
|
function buildInstrumentList() {
|
||||||
function updateCharts(metrics) {
|
const container = document.getElementById('instrument-list');
|
||||||
const dates = metrics.daily_pnl.map(d => d.date.substring(5)); // MM-DD
|
container.innerHTML = '';
|
||||||
const cumulative = metrics.daily_pnl.map(d => d.cumulative);
|
Object.entries(instrumentData).forEach(([symbol, data]) => {
|
||||||
const dailyPnl = metrics.daily_pnl.map(d => d.pnl);
|
const card = document.createElement('div');
|
||||||
|
card.className = 'instrument-card p-2 mb-2';
|
||||||
// Cumulative P&L Chart
|
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);
|
||||||
|
|
||||||
if (pnlChart) pnlChart.destroy();
|
if (pnlChart) pnlChart.destroy();
|
||||||
const ctx1 = document.getElementById('pnlChart').getContext('2d');
|
const ctx1 = document.getElementById('pnlChart').getContext('2d');
|
||||||
pnlChart = new Chart(ctx1, {
|
pnlChart = new Chart(ctx1, {
|
||||||
@@ -256,28 +508,34 @@
|
|||||||
datasets: [{
|
datasets: [{
|
||||||
label: 'Cumulative P&L',
|
label: 'Cumulative P&L',
|
||||||
data: cumulative,
|
data: cumulative,
|
||||||
borderColor: '#198754',
|
borderColor: '#22c55e',
|
||||||
backgroundColor: 'rgba(25, 135, 84, 0.1)',
|
backgroundColor: 'rgba(34,197,94,0.12)',
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4
|
tension: 0.35,
|
||||||
|
pointRadius: 0
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { display: true }
|
legend: { display: false }
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: { color: '#6b7280' },
|
||||||
|
grid: { color: 'rgba(31,41,55,0.4)' }
|
||||||
|
},
|
||||||
y: {
|
y: {
|
||||||
ticks: {
|
ticks: {
|
||||||
|
color: '#6b7280',
|
||||||
callback: value => formatCurrency(value)
|
callback: value => formatCurrency(value)
|
||||||
}
|
},
|
||||||
|
grid: { color: 'rgba(31,41,55,0.4)' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Daily Distribution Chart
|
|
||||||
if (distributionChart) distributionChart.destroy();
|
if (distributionChart) distributionChart.destroy();
|
||||||
const ctx2 = document.getElementById('dailyDistributionChart').getContext('2d');
|
const ctx2 = document.getElementById('dailyDistributionChart').getContext('2d');
|
||||||
const profitable = dailyPnl.filter(p => p > 0).length;
|
const profitable = dailyPnl.filter(p => p > 0).length;
|
||||||
@@ -288,19 +546,19 @@
|
|||||||
labels: ['Profitable Days', 'Unprofitable Days'],
|
labels: ['Profitable Days', 'Unprofitable Days'],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: [profitable, unprofitable],
|
data: [profitable, unprofitable],
|
||||||
backgroundColor: ['#198754', '#dc3545']
|
backgroundColor: ['#22c55e', '#ef4444'],
|
||||||
|
borderWidth: 0
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: { position: 'bottom' }
|
legend: { position: 'bottom', labels: { color: '#9ca3af' } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update table
|
|
||||||
function updateTable(dailyData) {
|
function updateTable(dailyData) {
|
||||||
const tbody = document.getElementById('daily-table');
|
const tbody = document.getElementById('daily-table');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
@@ -313,44 +571,65 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>${formatCurrency(day.cumulative)}</td>
|
<td>${formatCurrency(day.cumulative)}</td>
|
||||||
<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'}
|
${day.pnl >= 0 ? 'Profit' : 'Loss'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>`;
|
||||||
`;
|
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch metrics from API
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
async function fetchMetrics() {
|
buildInstrumentList();
|
||||||
try {
|
updateUI();
|
||||||
const response = await fetch('/api/metrics');
|
|
||||||
if (!response.ok) throw new Error('Network response was not ok');
|
document.getElementById('instrument-form').addEventListener('submit', (e) => {
|
||||||
const metrics = await response.json();
|
e.preventDefault();
|
||||||
updateUI(metrics);
|
const symbolInput = document.getElementById('instrument-symbol');
|
||||||
} catch (error) {
|
const typeSelect = document.getElementById('instrument-type');
|
||||||
console.error('Failed to fetch metrics:', error);
|
const symbol = symbolInput.value.trim().toUpperCase();
|
||||||
alert('Unable to load metrics. Please check server connection.');
|
const type = typeSelect.value;
|
||||||
}
|
if (!symbol) return;
|
||||||
}
|
|
||||||
|
if (!instrumentData[symbol]) {
|
||||||
// Refresh metrics
|
instrumentData[symbol] = {
|
||||||
function refreshMetrics() {
|
type,
|
||||||
const btn = event?.target || document.querySelector('button[onclick="refreshMetrics()"]');
|
label: type === 'equity' ? `${symbol} (Equity)` : `${symbol} (FX pair)`,
|
||||||
btn.innerHTML = '🔄 Loading...';
|
metrics: {
|
||||||
btn.disabled = true;
|
total_pnl: 0,
|
||||||
fetchMetrics().finally(() => {
|
max_drawdown: 0,
|
||||||
btn.innerHTML = '🔄 Refresh';
|
win_rate: 0.5,
|
||||||
btn.disabled = false;
|
portfolio_value: 10000,
|
||||||
|
peak: 10000,
|
||||||
|
trough: 10000,
|
||||||
|
kpiTrend: 'New',
|
||||||
|
kpiVolatility: 'Unknown'
|
||||||
|
},
|
||||||
|
daily_pnl: generateMockDaily(30, 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
selectedInstrument = symbol;
|
||||||
|
symbolInput.value = '';
|
||||||
|
buildInstrumentList();
|
||||||
|
updateUI();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
document.getElementById('refresh-btn').addEventListener('click', (e) => {
|
||||||
// Initialize on page load
|
e.preventDefault();
|
||||||
document.addEventListener('DOMContentLoaded', fetchMetrics);
|
const btn = e.currentTarget;
|
||||||
|
const original = btn.innerHTML;
|
||||||
// Auto-refresh every 30 seconds
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Simulating';
|
||||||
setInterval(fetchMetrics, 30000);
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user