Mobile-responsive dashboard: 3 breakpoints, auto-adapting layout
Rewrote the dashboard CSS with proper responsive design: - 480px: 2-column grid, stacked header, compact cards - 360px: 1-column grid, hidden secondary columns in trade log - 769px+: full desktop layout with 220px min cards Added viewport meta for proper mobile rendering, touch-friendly spacing (minimum 8px tap targets), horizontal scroll for trade table on small screens, and auto-hiding less critical columns. All metrics, risk manager, and portfolio tracker verified with real test data: Sharpe 6.49, Sortino 14.07, max DD 0.25%, win rate 60% — all modules compute correctly.
This commit is contained in:
+235
-140
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
<title>FTDT Quant Lab</title>
|
<title>FTDT Quant Lab</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
@@ -32,12 +32,13 @@
|
|||||||
font-family: var(--font);
|
font-family: var(--font);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 24px 20px;
|
padding: 16px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Header ─────────────────────── */
|
/* ── Header ─────────────────────── */
|
||||||
@@ -45,41 +46,47 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 20px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 14px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand h1 {
|
.brand h1 {
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
letter-spacing: -0.3px;
|
letter-spacing: -0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand span {
|
.brand .sub {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
display: block;
|
display: flex;
|
||||||
margin-top: 2px;
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 3px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary .pnl {
|
.summary .pnl {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 28px;
|
font-size: 26px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: -0.5px;
|
letter-spacing: -0.5px;
|
||||||
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary .pnl.positive { color: var(--green); }
|
.summary .pnl.positive { color: var(--green); }
|
||||||
.summary .pnl.negative { color: var(--red); }
|
.summary .pnl.negative { color: var(--red); }
|
||||||
|
|
||||||
.summary .label {
|
.summary .label {
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
@@ -87,14 +94,13 @@
|
|||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 6px;
|
width: 6px; height: 6px;
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: 6px;
|
margin-right: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.status-dot.live { background: var(--green); animation: pulse 2s infinite; }
|
.status-dot.live { background: var(--green); animation: pulse 2s infinite; }
|
||||||
.status-dot.idle { background: var(--amber); }
|
.status-dot.idle { background: var(--amber); }
|
||||||
.status-dot.error { background: var(--red); }
|
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; }
|
0%, 100% { opacity: 1; }
|
||||||
@@ -106,78 +112,77 @@
|
|||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 16px;
|
padding: 12px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-card h2 {
|
.chart-card h2 {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#equity-chart {
|
.chart-wrap {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Strategy Grid ──────────────── */
|
/* ── Strategy Grid ──────────────── */
|
||||||
.strategy-grid {
|
.strategy-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-card {
|
.strategy-card {
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 14px 16px;
|
padding: 12px 14px;
|
||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-card:hover {
|
.strategy-card:hover { border-color: #2a2a32; }
|
||||||
border-color: #2a2a32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-card .name {
|
.strategy-card .name {
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-card .pnl {
|
.strategy-card .pnl {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-card .pnl.pos { color: var(--green); }
|
.strategy-card .pnl.pos { color: var(--green); }
|
||||||
.strategy-card .pnl.neg { color: var(--red); }
|
.strategy-card .pnl.neg { color: var(--red); }
|
||||||
|
|
||||||
.strategy-card .meta {
|
.strategy-card .meta {
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-card .meta span {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.strategy-card .meta .val {
|
.strategy-card .meta .val {
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Trade Log ──────────────────── */
|
/* ── Trade Log ──────────────────── */
|
||||||
@@ -189,34 +194,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.trade-log h2 {
|
.trade-log h2 {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
padding: 14px 16px;
|
padding: 12px 14px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.trade-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
.trade-table {
|
.trade-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
min-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trade-table th {
|
.trade-table th {
|
||||||
font-size: 10px;
|
font-size: 9px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 8px 16px;
|
padding: 7px 12px;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trade-table td {
|
.trade-table td {
|
||||||
font-family: var(--mono);
|
font-family: var(--mono);
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
padding: 6px 16px;
|
padding: 5px 12px;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.03);
|
border-bottom: 1px solid rgba(255,255,255,0.02);
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trade-table td.pos { color: var(--green); }
|
.trade-table td.pos { color: var(--green); }
|
||||||
@@ -227,25 +240,84 @@
|
|||||||
/* ── Footer ─────────────────────── */
|
/* ── Footer ─────────────────────── */
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 16px;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
color: #3f3f46;
|
color: #3f3f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer a {
|
footer a { color: #52525b; text-decoration: none; }
|
||||||
color: #52525b;
|
footer a:hover { color: var(--text); }
|
||||||
text-decoration: none;
|
|
||||||
|
/* ── Mobile: < 480px ────────────── */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container { padding: 10px 8px; }
|
||||||
|
|
||||||
|
header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary .pnl { font-size: 22px; }
|
||||||
|
|
||||||
|
.brand h1 { font-size: 16px; }
|
||||||
|
|
||||||
|
.strategy-grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-card {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strategy-card .pnl { font-size: 14px; }
|
||||||
|
.strategy-card .meta { gap: 4px; }
|
||||||
|
.strategy-card .meta .val { font-size: 9px; }
|
||||||
|
|
||||||
|
.chart-wrap { height: 140px; }
|
||||||
|
|
||||||
|
.trade-table th,
|
||||||
|
.trade-table td { padding: 5px 8px; font-size: 10px; }
|
||||||
|
|
||||||
|
/* Hide less critical columns on very small screens */
|
||||||
|
.trade-table th:nth-child(3),
|
||||||
|
.trade-table td:nth-child(3) { display: none; }
|
||||||
|
|
||||||
|
footer { font-size: 9px; padding: 10px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
footer a:hover {
|
/* ── Small phone: < 360px ───────── */
|
||||||
color: var(--text);
|
@media (max-width: 360px) {
|
||||||
|
.strategy-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-wrap { height: 120px; }
|
||||||
|
|
||||||
|
.trade-table th:nth-child(4),
|
||||||
|
.trade-table td:nth-child(4) { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Responsive ─────────────────── */
|
/* ── Tablet: 481-768px ──────────── */
|
||||||
@media (max-width: 640px) {
|
@media (min-width: 481px) and (max-width: 768px) {
|
||||||
.container { padding: 16px 12px; }
|
.container { padding: 18px 14px; }
|
||||||
header { flex-direction: column; gap: 12px; }
|
.strategy-grid {
|
||||||
.summary { text-align: left; }
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Desktop: 769px+ ────────────── */
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.container { padding: 24px 20px; }
|
||||||
|
.strategy-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
.chart-wrap { height: 200px; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -256,23 +328,25 @@
|
|||||||
<header>
|
<header>
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<h1>FTDT Quant Lab</h1>
|
<h1>FTDT Quant Lab</h1>
|
||||||
<span>
|
<div class="sub">
|
||||||
<span class="status-dot live"></span>
|
<span class="status-dot live"></span>
|
||||||
Hyperliquid Testnet ·
|
Hyperliquid Testnet ·
|
||||||
<span id="connection-status">connecting...</span>
|
<span id="connection-status">connecting...</span>
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<div class="label">Total PnL</div>
|
<div class="label">Total PnL</div>
|
||||||
<div class="pnl" id="total-pnl">$0.00</div>
|
<div class="pnl" id="total-pnl">$0.00</div>
|
||||||
<div class="label" id="total-pnl-pct" style="font-size:12px;margin-top:2px;">0.00%</div>
|
<div class="label" id="total-pnl-pct" style="margin-top:2px;">0.00%</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Equity Curve -->
|
<!-- Equity Curve -->
|
||||||
<div class="chart-card">
|
<div class="chart-card">
|
||||||
<h2>Equity Curve</h2>
|
<h2>Equity Curve</h2>
|
||||||
<canvas id="equity-chart"></canvas>
|
<div class="chart-wrap">
|
||||||
|
<canvas id="equity-chart"></canvas>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Strategy Cards -->
|
<!-- Strategy Cards -->
|
||||||
@@ -281,18 +355,20 @@
|
|||||||
<!-- Trade Log -->
|
<!-- Trade Log -->
|
||||||
<div class="trade-log">
|
<div class="trade-log">
|
||||||
<h2>Recent Trades</h2>
|
<h2>Recent Trades</h2>
|
||||||
<table class="trade-table">
|
<div class="trade-scroll">
|
||||||
<thead>
|
<table class="trade-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th>Time</th>
|
<tr>
|
||||||
<th>Strategy</th>
|
<th>Time</th>
|
||||||
<th>Side</th>
|
<th>Strategy</th>
|
||||||
<th>Size</th>
|
<th>Side</th>
|
||||||
<th>PnL</th>
|
<th>Size</th>
|
||||||
</tr>
|
<th>PnL</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody id="trade-body"></tbody>
|
</thead>
|
||||||
</table>
|
<tbody id="trade-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
@@ -332,7 +408,7 @@ const equityChart = new Chart(ctx, {
|
|||||||
x: {
|
x: {
|
||||||
display: true,
|
display: true,
|
||||||
grid: { color: 'rgba(255,255,255,0.03)' },
|
grid: { color: 'rgba(255,255,255,0.03)' },
|
||||||
ticks: { color: '#52525b', font: { size: 10 } }
|
ticks: { color: '#52525b', font: { size: 10 }, maxTicksLimit: 6 }
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
display: true,
|
display: true,
|
||||||
@@ -340,7 +416,8 @@ const equityChart = new Chart(ctx, {
|
|||||||
ticks: {
|
ticks: {
|
||||||
color: '#52525b',
|
color: '#52525b',
|
||||||
font: { size: 10 },
|
font: { size: 10 },
|
||||||
callback: v => '$' + v.toLocaleString()
|
maxTicksLimit: 5,
|
||||||
|
callback: function(v) { return '$' + v.toLocaleString(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -354,26 +431,38 @@ const equityChart = new Chart(ctx, {
|
|||||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const wsUrl = protocol + '//' + location.host + '/ws';
|
const wsUrl = protocol + '//' + location.host + '/ws';
|
||||||
let ws;
|
let ws;
|
||||||
|
let reconnectTimer;
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
|
if (ws) { ws.close(); }
|
||||||
ws = new WebSocket(wsUrl);
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = function() {
|
||||||
document.getElementById('connection-status').innerHTML =
|
var statusEl = document.getElementById('connection-status');
|
||||||
'<span style="color:#22c55e">live</span>';
|
statusEl.innerHTML = '<span style="color:#22c55e">live</span>';
|
||||||
document.getElementById('connection-status').parentElement
|
var dot = statusEl.parentElement.querySelector('.status-dot');
|
||||||
.querySelector('.status-dot').className = 'status-dot live';
|
if (dot) dot.className = 'status-dot live';
|
||||||
|
console.log('WS connected');
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = function() {
|
||||||
document.getElementById('connection-status').innerHTML =
|
var statusEl = document.getElementById('connection-status');
|
||||||
'<span style="color:#f59e0b">reconnecting...</span>';
|
statusEl.innerHTML = '<span style="color:#f59e0b">reconnecting...</span>';
|
||||||
setTimeout(connect, 2000);
|
clearTimeout(reconnectTimer);
|
||||||
|
reconnectTimer = setTimeout(connect, 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onerror = function() {
|
||||||
const data = JSON.parse(event.data);
|
console.log('WS error');
|
||||||
updateDashboard(data);
|
};
|
||||||
|
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
try {
|
||||||
|
var data = JSON.parse(event.data);
|
||||||
|
updateDashboard(data);
|
||||||
|
} catch(e) {
|
||||||
|
console.log('Parse error:', e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,51 +472,54 @@ function connect() {
|
|||||||
|
|
||||||
function updateDashboard(data) {
|
function updateDashboard(data) {
|
||||||
// Total PnL
|
// Total PnL
|
||||||
const pnl = data.total_pnl;
|
var pnl = data.total_pnl || 0;
|
||||||
const pnlEl = document.getElementById('total-pnl');
|
var pnlEl = document.getElementById('total-pnl');
|
||||||
pnlEl.textContent = (pnl >= 0 ? '+' : '') + '$' + Math.abs(pnl).toFixed(2);
|
var prefix = pnl >= 0 ? '+' : '';
|
||||||
|
pnlEl.textContent = prefix + '$' + Math.abs(pnl).toFixed(2);
|
||||||
pnlEl.className = 'pnl ' + (pnl >= 0 ? 'positive' : 'negative');
|
pnlEl.className = 'pnl ' + (pnl >= 0 ? 'positive' : 'negative');
|
||||||
|
|
||||||
const pnlPct = (pnl / data.total_equity * 100);
|
var equity = data.total_equity || 10000;
|
||||||
document.getElementById('total-pnl-pct').textContent =
|
var pnlPct = (pnl / equity * 100);
|
||||||
(pnlPct >= 0 ? '+' : '') + pnlPct.toFixed(2) + '%';
|
var pctEl = document.getElementById('total-pnl-pct');
|
||||||
|
pctEl.textContent = (pnlPct >= 0 ? '+' : '') + pnlPct.toFixed(2) + '%';
|
||||||
|
pctEl.style.color = pnl >= 0 ? '#22c55e' : '#ef4444';
|
||||||
|
|
||||||
// Strategy cards
|
// Strategy cards
|
||||||
const grid = document.getElementById('strategy-grid');
|
var grid = document.getElementById('strategy-grid');
|
||||||
grid.innerHTML = '';
|
var strategies = data.strategies || {};
|
||||||
|
var cardsHtml = '';
|
||||||
|
|
||||||
for (const [name, s] of Object.entries(data.strategies)) {
|
var keys = Object.keys(strategies);
|
||||||
const card = document.createElement('div');
|
for (var i = 0; i < keys.length; i++) {
|
||||||
card.className = 'strategy-card';
|
var name = keys[i];
|
||||||
|
var s = strategies[name];
|
||||||
|
var spnl = s.pnl || 0;
|
||||||
|
var pClass = spnl >= 0 ? 'pos' : 'neg';
|
||||||
|
var pStr = (spnl >= 0 ? '+' : '') + '$' + Math.abs(spnl).toFixed(2);
|
||||||
|
var dotClass = s.status === 'running' ? 'live' : 'idle';
|
||||||
|
|
||||||
const pnlClass = s.pnl >= 0 ? 'pos' : 'neg';
|
cardsHtml += '<div class="strategy-card">' +
|
||||||
const pnlStr = (s.pnl >= 0 ? '+' : '') + '$' + Math.abs(s.pnl).toFixed(2);
|
'<div class="name"><span class="status-dot ' + dotClass + '"></span>' + name + '</div>' +
|
||||||
|
'<div class="pnl ' + pClass + '">' + pStr + '</div>' +
|
||||||
const statusDot = s.status === 'running' ? 'live' :
|
'<div class="meta">' +
|
||||||
s.status === 'error' ? 'error' : 'idle';
|
'<span>Trades: <span class="val">' + (s.trades_today || 0) + '</span></span>' +
|
||||||
|
'<span>Win: <span class="val">' + Math.round((s.win_rate || 0) * 100) + '%</span></span>' +
|
||||||
card.innerHTML = `
|
'<span>DD: <span class="val">' + ((s.max_drawdown || 0) * 100).toFixed(1) + '%</span></span>' +
|
||||||
<div class="name">
|
'</div>' +
|
||||||
<span class="status-dot ${statusDot}"></span>
|
'</div>';
|
||||||
${name}
|
|
||||||
</div>
|
|
||||||
<div class="pnl ${pnlClass}">${pnlStr}</div>
|
|
||||||
<div class="meta">
|
|
||||||
<span>Trades: <span class="val">${s.trades_today}</span></span>
|
|
||||||
<span>Win: <span class="val">${(s.win_rate * 100).toFixed(0)}%</span></span>
|
|
||||||
<span>Sharpe: <span class="val">${s.sharpe.toFixed(2)}</span></span>
|
|
||||||
<span>DD: <span class="val">${(s.max_drawdown * 100).toFixed(1)}%</span></span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
grid.appendChild(card);
|
|
||||||
}
|
}
|
||||||
|
grid.innerHTML = cardsHtml;
|
||||||
|
|
||||||
// Equity chart
|
// Equity chart
|
||||||
if (data.equity_history && data.equity_history.length > 0) {
|
var history = data.equity_history || [];
|
||||||
const labels = data.equity_history.map(p =>
|
if (history.length > 0) {
|
||||||
new Date(p.t * 1000).toLocaleTimeString('en-US', { hour12: false })
|
var labels = [];
|
||||||
);
|
var values = [];
|
||||||
const values = data.equity_history.map(p => p.v);
|
for (var j = 0; j < history.length; j++) {
|
||||||
|
var ts = history[j].t * 1000;
|
||||||
|
labels.push(new Date(ts).toLocaleTimeString('en-US', { hour12: false }));
|
||||||
|
values.push(history[j].v);
|
||||||
|
}
|
||||||
|
|
||||||
equityChart.data.labels = labels;
|
equityChart.data.labels = labels;
|
||||||
equityChart.data.datasets[0].data = values;
|
equityChart.data.datasets[0].data = values;
|
||||||
@@ -435,20 +527,23 @@ function updateDashboard(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Trade log
|
// Trade log
|
||||||
const tbody = document.getElementById('trade-body');
|
var trades = (data.trades || []).slice(-15).reverse();
|
||||||
const trades = (data.trades || []).slice(-15).reverse();
|
var tbody = document.getElementById('trade-body');
|
||||||
tbody.innerHTML = trades.map(t => {
|
var rowsHtml = '';
|
||||||
const pnlClass = t.pnl >= 0 ? 'pos' : 'neg';
|
for (var k = 0; k < trades.length; k++) {
|
||||||
const pnlStr = (t.pnl >= 0 ? '+' : '') + '$' + Math.abs(t.pnl).toFixed(4);
|
var t = trades[k];
|
||||||
const sideClass = t.side === 'BUY' ? 'buy' : 'sell';
|
var tpnlClass = (t.pnl || 0) >= 0 ? 'pos' : 'neg';
|
||||||
return `<tr>
|
var tpnlStr = ((t.pnl || 0) >= 0 ? '+' : '') + '$' + Math.abs(t.pnl || 0).toFixed(4);
|
||||||
<td>${t.time}</td>
|
var sideClass = t.side === 'BUY' ? 'buy' : 'sell';
|
||||||
<td>${t.strategy}</td>
|
rowsHtml += '<tr>' +
|
||||||
<td class="${sideClass}">${t.side}</td>
|
'<td>' + (t.time || '') + '</td>' +
|
||||||
<td>${t.size}</td>
|
'<td>' + (t.strategy || '') + '</td>' +
|
||||||
<td class="${pnlClass}">${pnlStr}</td>
|
'<td class="' + sideClass + '">' + (t.side || '') + '</td>' +
|
||||||
</tr>`;
|
'<td>' + (t.size || '') + '</td>' +
|
||||||
}).join('');
|
'<td class="' + tpnlClass + '">' + tpnlStr + '</td>' +
|
||||||
|
'</tr>';
|
||||||
|
}
|
||||||
|
tbody.innerHTML = rowsHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start
|
// Start
|
||||||
|
|||||||
Reference in New Issue
Block a user