feat: VBT overview + light mode + /api/vbt/summary
dashboard/static/vbt.html — complete rewrite:
- CSS variables for theming (--bg, --text, --blue, etc.)
- Light mode toggle (☀/☾) persisted in localStorage
- Plotly charts update colors on theme switch
- Overview panel on load: strategy cards + heatmap
- Strategy cards: best Sharpe, best return, top combo,
+Sharpe %, run count. Click to filter.
- Heatmap: color-coded strategy×interval table
(green>2, green>0.5, yellow>0, orange>-0.5, red)
- Back-to-overview button on detail view
- Default sort: Sharpe (was date)
dashboard/server.py:
- New /api/vbt/summary endpoint:
Per-strategy best (Sharpe, Return, Calmar, trades,
positive-Sharpe %, top combo)
Heatmap: strategy×interval best Sharpe matrix
Tests: 241 passing (excl. Hawkes)
This commit is contained in:
+150
-187
@@ -1,98 +1,102 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" class="">
|
||||||
<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">
|
||||||
<title>FTDT Quant Lab — VBT Dashboard</title>
|
<title>FTDT Quant Lab — VBT Dashboard</title>
|
||||||
<script src="https://cdn.plot.ly/plotly-3.1.0.min.js"></script>
|
<script src="https://cdn.plot.ly/plotly-3.1.0.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
|
:root{--bg:#0a0a0a;--bg2:#111;--bg3:#181818;--border:#222;--border2:#333;--text:#888;--text2:#ccc;--text3:#ddd;--text4:#555;--sidebar:#0d0d0d;--blue:#03A9F4;--red:#FF5252;--green:#4CAF50;--chart-bg:rgba(0,0,0,0);--chart-grid:#1a1a1a;--chart-tick:#444}
|
||||||
|
.light{--bg:#f5f5f5;--bg2:#fff;--bg3:#e8e8e8;--border:#d8d8d8;--border2:#bbb;--text:#777;--text2:#333;--text3:#111;--text4:#999;--sidebar:#fafafa;--blue:#0288d1;--red:#c62828;--green:#2e7d32;--chart-bg:rgba(255,255,255,0);--chart-grid:#eee;--chart-tick:#aaa}
|
||||||
*{margin:0;padding:0;box-sizing:border-box}
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
body{font-family:'SF Mono','Cascadia Code','Ubuntu Mono',monospace;background:#0a0a0a;color:#888;min-height:100vh;overflow:hidden}
|
body{font-family:'SF Mono','Cascadia Code','Ubuntu Mono',monospace;background:var(--bg);color:var(--text);min-height:100vh;overflow:hidden}
|
||||||
.pos{color:#03A9F4}.neg{color:#FF5252}.neu{color:#777}.warn{color:#f59e0b}
|
.pos{color:var(--blue)}.neg{color:var(--red)}.neu{color:var(--text)}.warn{color:#f59e0b}
|
||||||
.topbar{background:#111;border-bottom:1px solid #222;padding:8px 20px;display:flex;justify-content:space-between;align-items:center;height:40px}
|
.topbar{background:var(--bg2);border-bottom:1px solid var(--border);padding:8px 20px;display:flex;justify-content:space-between;align-items:center;height:40px}
|
||||||
.topbar h1{font-size:13px;font-weight:600;color:#ddd;letter-spacing:1px}
|
.topbar h1{font-size:13px;font-weight:600;color:var(--text3);letter-spacing:1px}
|
||||||
.topbar .dot{display:inline-block;width:6px;height:6px;background:#fff;border-radius:50%;margin-right:5px;animation:pulse 2s infinite}
|
.topbar .dot{display:inline-block;width:6px;height:6px;background:var(--blue);border-radius:50%;margin-right:5px;animation:pulse 2s infinite}
|
||||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
||||||
.topbar .status{font-size:10px;color:#999}
|
.theme-btn{font-size:10px;cursor:pointer;background:var(--bg3);border:1px solid var(--border);color:var(--text);padding:3px 8px;border-radius:3px;font-family:inherit;transition:all .12s}
|
||||||
|
.theme-btn:hover{background:var(--bg2);border-color:var(--border2)}
|
||||||
.main{display:grid;grid-template-columns:320px 1fr;height:calc(100vh - 40px)}
|
.main{display:grid;grid-template-columns:320px 1fr;height:calc(100vh - 40px)}
|
||||||
.sidebar{background:#0d0d0d;border-right:1px solid #222;overflow-y:auto;padding:6px;display:flex;flex-direction:column}
|
.sidebar{background:var(--sidebar);border-right:1px solid var(--border);overflow-y:auto;padding:6px;display:flex;flex-direction:column}
|
||||||
.controls{padding:6px;border-bottom:1px solid #222;margin-bottom:4px;flex-shrink:0}
|
.controls{padding:6px;border-bottom:1px solid var(--border);margin-bottom:4px;flex-shrink:0}
|
||||||
.controls .row{display:flex;gap:3px;margin-bottom:3px}
|
.controls .row{display:flex;gap:3px;margin-bottom:3px}
|
||||||
select{background:#181818;border:1px solid #333;color:#aaa;border-radius:3px;padding:5px 6px;font-size:10px;font-family:inherit;cursor:pointer;flex:1;min-width:0}
|
select{background:var(--bg3);border:1px solid var(--border2);color:var(--text);border-radius:3px;padding:5px 6px;font-size:10px;font-family:inherit;cursor:pointer;flex:1;min-width:0}
|
||||||
select:focus{outline:none;border-color:#555}
|
select:focus{outline:none;border-color:var(--text4)}
|
||||||
.btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;padding:4px 7px;border-radius:3px;font-size:10px;cursor:pointer;border:1px solid #333;background:#181818;color:#aaa;font-family:inherit;transition:all .12s;white-space:nowrap}
|
.btn{display:inline-flex;align-items:center;justify-content:center;gap:3px;padding:4px 7px;border-radius:3px;font-size:10px;cursor:pointer;border:1px solid var(--border2);background:var(--bg3);color:var(--text);font-family:inherit;transition:all .12s;white-space:nowrap}
|
||||||
.btn:hover{background:#222;border-color:#555}
|
.btn:hover{background:var(--bg2);border-color:var(--text4)}
|
||||||
.btn-primary{background:#03A9F4;border-color:#03A9F4;color:#fff}
|
.btn-primary{background:var(--blue);border-color:var(--blue);color:#fff}
|
||||||
.btn-primary:hover{background:#0288d1}
|
.btn-primary:hover{opacity:.85}
|
||||||
.btn-success{background:#4CAF50;border-color:#4CAF50;color:#fff}
|
.btn-success{background:var(--green);border-color:var(--green);color:#fff}
|
||||||
.btn-success:hover{background:#388E3C}
|
.btn-success:hover{opacity:.85}
|
||||||
.btn:disabled{opacity:.4;cursor:not-allowed}
|
.btn:disabled{opacity:.4;cursor:not-allowed}
|
||||||
.result-item{background:#111;border:1px solid #222;border-left:3px solid transparent;border-radius:3px;padding:7px 8px;margin-bottom:2px;cursor:pointer;transition:all .12s}
|
.result-item{background:var(--bg2);border:1px solid var(--border);border-left:3px solid transparent;border-radius:3px;padding:7px 8px;margin-bottom:2px;cursor:pointer;transition:all .12s}
|
||||||
.result-item:hover{border-color:#333;background:#161616}
|
.result-item:hover{border-color:var(--border2);background:var(--bg3)}
|
||||||
.result-item.active{border-color:#03A9F4;border-left-color:#03A9F4;background:#121212}
|
.result-item.active{border-color:var(--blue);border-left-color:var(--blue)}
|
||||||
.result-item .name{font-size:11px;font-weight:600;color:#ccc;display:flex;justify-content:space-between;align-items:center}
|
.result-item .name{font-size:11px;font-weight:600;color:var(--text2);display:flex;justify-content:space-between;align-items:center}
|
||||||
.result-item .name .asset{font-size:9px;color:#03A9F4;background:#0d1f2b;border:1px solid #1a3a4a;border-radius:3px;padding:1px 5px}
|
.result-item .name .asset{font-size:9px;color:var(--blue);background:var(--bg3);border:1px solid var(--border);border-radius:3px;padding:1px 5px}
|
||||||
.result-item .meta{font-size:9px;color:#555;margin-top:2px}
|
.result-item .meta{font-size:9px;color:var(--text4);margin-top:2px}
|
||||||
.result-item .stats{display:flex;gap:8px;margin-top:3px;font-size:9px;font-family:monospace;flex-wrap:wrap}
|
.result-item .stats{display:flex;gap:8px;margin-top:3px;font-size:9px;font-family:monospace;flex-wrap:wrap}
|
||||||
.sidebar h3{font-size:9px;text-transform:uppercase;color:#555;margin:8px 0 4px;letter-spacing:2px;padding:0 4px}
|
.sidebar h3{font-size:9px;text-transform:uppercase;color:var(--text4);margin:8px 0 4px;letter-spacing:2px;padding:0 4px}
|
||||||
.badge{font-size:9px;border-radius:3px;padding:1px 4px;margin-left:3px;font-weight:400}
|
.badge{font-size:9px;border-radius:3px;padding:1px 4px;margin-left:3px;font-weight:400}
|
||||||
#results-list{flex:1;overflow-y:auto;min-height:0}
|
#results-list{flex:1;overflow-y:auto;min-height:0}
|
||||||
.pagination-row{display:flex;align-items:center;justify-content:space-between;padding:4px 4px 2px;border-top:1px solid #1a1a1a;flex-shrink:0}
|
.pagination-row{display:flex;align-items:center;justify-content:space-between;padding:4px 4px 2px;border-top:1px solid var(--border);flex-shrink:0}
|
||||||
.pagination-row .count{font-size:9px;color:#555}
|
.pagination-row .count{font-size:9px;color:var(--text4)}
|
||||||
.pagination-row .nav-btn{font-size:9px;padding:2px 6px;cursor:pointer;border:1px solid #333;background:#181818;color:#aaa;border-radius:3px;font-family:inherit}
|
.pagination-row .nav-btn{font-size:9px;padding:2px 6px;cursor:pointer;border:1px solid var(--border2);background:var(--bg3);color:var(--text);border-radius:3px;font-family:inherit}
|
||||||
.pagination-row .nav-btn:hover{background:#222;border-color:#555}
|
.pagination-row .nav-btn:hover{background:var(--bg2)}
|
||||||
.pagination-row .nav-btn:disabled{opacity:.3;cursor:default}
|
.pagination-row .nav-btn:disabled{opacity:.3;cursor:default}
|
||||||
|
|
||||||
.content{padding:20px 28px;overflow-y:auto}
|
.content{padding:20px 28px;overflow-y:auto}
|
||||||
.content h2{font-size:15px;color:#ddd;margin-bottom:2px}
|
.content h2{font-size:15px;color:var(--text3);margin-bottom:2px}
|
||||||
.content .sub{font-size:10px;color:#555;margin-bottom:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
.content .sub{font-size:10px;color:var(--text4);margin-bottom:8px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||||
.metrics-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-bottom:10px}
|
.metrics-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-bottom:10px}
|
||||||
.metric{background:#111;border:1px solid #222;border-radius:4px;padding:10px 12px}
|
.metric{background:var(--bg2);border:1px solid var(--border);border-radius:4px;padding:10px 12px}
|
||||||
.metric .label{font-size:8px;text-transform:uppercase;color:#555;letter-spacing:1.5px;margin-bottom:3px}
|
.metric .label{font-size:8px;text-transform:uppercase;color:var(--text4);letter-spacing:1.5px;margin-bottom:3px}
|
||||||
.metric .value{font-size:18px;font-weight:700;font-family:monospace}
|
.metric .value{font-size:18px;font-weight:700;font-family:monospace}
|
||||||
.chart-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px}
|
.chart-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px}
|
||||||
.chart-box{background:#111;border:1px solid #222;border-radius:4px;padding:12px}
|
.chart-box{background:var(--bg2);border:1px solid var(--border);border-radius:4px;padding:12px}
|
||||||
.chart-box h4{font-size:9px;color:#555;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:2px}
|
.chart-box h4{font-size:9px;color:var(--text4);text-transform:uppercase;letter-spacing:1.5px;margin-bottom:2px}
|
||||||
.chart-box.full{grid-column:1/-1}
|
.chart-box.full{grid-column:1/-1}
|
||||||
.empty{text-align:center;padding:60px 20px;color:#333}
|
.empty{text-align:center;padding:60px 20px;color:var(--text4)}
|
||||||
.empty h2{font-size:14px;margin-bottom:6px;color:#555}
|
.empty h2{font-size:14px;margin-bottom:6px;color:var(--text4)}
|
||||||
.loading{text-align:center;padding:14px;color:#333;font-size:10px}
|
.loading{text-align:center;padding:14px;color:var(--text4);font-size:10px}
|
||||||
.error-box{background:#1a0a0a;border:1px solid #551111;border-radius:4px;padding:14px;text-align:center}
|
.error-box{background:var(--bg2);border:1px solid var(--red);border-radius:4px;padding:14px;text-align:center}
|
||||||
.error-box h3{color:#FF5252;font-size:12px;margin-bottom:6px}
|
.error-box h3{color:var(--red);font-size:12px;margin-bottom:6px}
|
||||||
.error-box button{margin-top:8px}
|
|
||||||
.params-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px;margin-bottom:10px}
|
.params-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px;margin-bottom:10px}
|
||||||
.param{background:#111;border:1px solid #222;border-radius:3px;padding:8px 10px}
|
.param{background:var(--bg2);border:1px solid var(--border);border-radius:3px;padding:8px 10px}
|
||||||
.param .k{font-size:9px;color:#555;text-transform:uppercase;margin-bottom:2px}
|
.param .k{font-size:9px;color:var(--text4);text-transform:uppercase;margin-bottom:2px}
|
||||||
.param .v{font-size:13px;color:#03A9F4;font-family:monospace;font-weight:600}
|
.param .v{font-size:13px;color:var(--blue);font-family:monospace;font-weight:600}
|
||||||
.trades-table{width:100%;border-collapse:collapse;font-size:11px;font-family:monospace}
|
.trades-table{width:100%;border-collapse:collapse;font-size:11px;font-family:monospace}
|
||||||
.trades-table th{text-align:left;padding:7px 10px;border-bottom:1px solid #222;color:#555;font-weight:500;font-size:9px;text-transform:uppercase;letter-spacing:1px;position:sticky;top:0;background:#111;z-index:1}
|
.trades-table th{text-align:left;padding:7px 10px;border-bottom:1px solid var(--border);color:var(--text4);font-weight:500;font-size:9px;text-transform:uppercase;letter-spacing:1px;position:sticky;top:0;background:var(--bg2);z-index:1}
|
||||||
.trades-table td{padding:5px 10px;border-bottom:1px solid #1a1a1a;color:#aaa}
|
.trades-table td{padding:5px 10px;border-bottom:1px solid var(--border);color:var(--text)}
|
||||||
.trades-table tr:hover{background:#151515}
|
.trades-table tr:hover{background:var(--bg3)}
|
||||||
.trades-table .pnl-pos{color:#03A9F4}.trades-table .pnl-neg{color:#FF5252}
|
.trades-table .pnl-pos{color:var(--blue)}.trades-table .pnl-neg{color:var(--red)}
|
||||||
.trade-scroll{max-height:400px;overflow-y:auto;border:1px solid #222;border-radius:4px}
|
.trade-scroll{max-height:400px;overflow-y:auto;border:1px solid var(--border);border-radius:4px}
|
||||||
.export-row{display:flex;gap:6px;margin-bottom:6px}
|
.export-row{display:flex;gap:6px;margin-bottom:6px}
|
||||||
.progress-bar{height:4px;background:#03A9F4;border-radius:2px;transition:width .3s;margin-top:4px}
|
.progress-bar{height:4px;background:var(--blue);border-radius:2px;transition:width .3s;margin-top:4px}
|
||||||
.run-status{font-size:10px;color:#f59e0b;padding:4px 0}
|
.run-status{font-size:10px;color:#f59e0b;padding:4px 0}
|
||||||
.heatmap-table{border-collapse:collapse;font-size:10px;font-family:monospace;width:100%}
|
.heatmap-table{border-collapse:collapse;font-size:10px;font-family:monospace;width:100%}
|
||||||
.heatmap-table th{text-align:center;padding:3px 6px;color:#555;font-weight:500;font-size:8px;text-transform:uppercase;letter-spacing:1px}
|
.heatmap-table th{text-align:center;padding:3px 6px;color:var(--text4);font-weight:500;font-size:8px;text-transform:uppercase;letter-spacing:1px}
|
||||||
.heatmap-table th:first-child{text-align:left;min-width:130px}
|
.heatmap-table th:first-child{text-align:left;min-width:130px}
|
||||||
.heatmap-table td{text-align:center;padding:3px 6px;border:1px solid #1a1a1a;font-size:10px}
|
.heatmap-table td{text-align:center;padding:3px 6px;border:1px solid var(--border);font-size:10px}
|
||||||
.heatmap-table td:first-child{text-align:left;color:#aaa;font-size:10px}
|
.heatmap-table td:first-child{text-align:left;color:var(--text);font-size:10px}
|
||||||
.heatmap-cell{cursor:pointer;transition:all .12s;border-radius:2px}
|
.heatmap-cell{cursor:pointer;transition:all .12s;border-radius:2px}
|
||||||
.heatmap-cell:hover{outline:1px solid #555;z-index:1;position:relative}
|
.heatmap-cell:hover{outline:1px solid var(--text4);z-index:1;position:relative}
|
||||||
.strat-summary{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:6px;margin-bottom:10px}
|
.strat-summary{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:6px;margin-bottom:10px}
|
||||||
.strat-card{background:#111;border:1px solid #222;border-radius:3px;padding:7px 9px;cursor:pointer;transition:all .12s}
|
.strat-card{background:var(--bg2);border:1px solid var(--border);border-radius:3px;padding:7px 9px;cursor:pointer;transition:all .12s}
|
||||||
.strat-card:hover{border-color:#444;background:#161616}
|
.strat-card:hover{background:var(--bg3);border-color:var(--border2)}
|
||||||
.strat-card .strat-name{font-size:10px;font-weight:600;color:#ccc;margin-bottom:4px}
|
.strat-card .strat-name{font-size:10px;font-weight:600;color:var(--text2);margin-bottom:4px}
|
||||||
.strat-card .strat-row{display:flex;justify-content:space-between;font-size:9px;padding:1px 0;color:#666}
|
.strat-card .strat-row{display:flex;justify-content:space-between;font-size:9px;padding:1px 0;color:var(--text4)}
|
||||||
.strat-card .strat-row .val{font-family:monospace;color:#888}
|
.strat-card .strat-row .val{font-family:monospace;color:var(--text)}
|
||||||
.strat-card .strat-row .val.pos{color:#03A9F4}.strat-card .strat-row .val.neg{color:#FF5252}
|
.strat-card .strat-row .val.pos{color:var(--blue)}.strat-card .strat-row .val.neg{color:var(--red)}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="topbar">
|
<div class="topbar">
|
||||||
<h1><span class="dot"></span>FTDT QUANT LAB — VBT</h1>
|
<h1><span class="dot"></span>FTDT QUANT LAB — VBT</h1>
|
||||||
<div class="status">Hyperliquid · VectorBT</div>
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<div class="status">639 results · Hyperliquid</div>
|
||||||
|
<button class="theme-btn" onclick="toggleTheme()" id="theme-btn">☀</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
@@ -124,8 +128,8 @@ select:focus{outline:none;border-color:#555}
|
|||||||
<option value="1h">1h</option><option value="4h">4h</option><option value="1d">1d</option>
|
<option value="1h">1h</option><option value="4h">4h</option><option value="1d">1d</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="filter-sort">
|
<select id="filter-sort">
|
||||||
|
<option value="sharpe" selected>Sharpe</option>
|
||||||
<option value="date">Latest</option>
|
<option value="date">Latest</option>
|
||||||
<option value="sharpe">Sharpe</option>
|
|
||||||
<option value="return">Return %</option>
|
<option value="return">Return %</option>
|
||||||
<option value="calmar">Calmar</option>
|
<option value="calmar">Calmar</option>
|
||||||
<option value="dd">Min DD</option>
|
<option value="dd">Min DD</option>
|
||||||
@@ -134,7 +138,7 @@ select:focus{outline:none;border-color:#555}
|
|||||||
</div>
|
</div>
|
||||||
<div class="row" style="margin-top:2px">
|
<div class="row" style="margin-top:2px">
|
||||||
<select id="fee-tier" style="flex:0.6" onchange="recalcIfSelected()">
|
<select id="fee-tier" style="flex:0.6" onchange="recalcIfSelected()">
|
||||||
<option value="0" selected>T0 (Base 0.045%/0.015%)</option>
|
<option value="0" selected>T0 (0.045%/0.015%)</option>
|
||||||
<option value="1">T1 (>$5M 0.040/0.012)</option>
|
<option value="1">T1 (>$5M 0.040/0.012)</option>
|
||||||
<option value="2">T2 (>$25M 0.035/0.008)</option>
|
<option value="2">T2 (>$25M 0.035/0.008)</option>
|
||||||
<option value="3">T3 (>$100M 0.030/0.004)</option>
|
<option value="3">T3 (>$100M 0.030/0.004)</option>
|
||||||
@@ -166,17 +170,9 @@ select:focus{outline:none;border-color:#555}
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<select id="run-interval">
|
<select id="run-interval"><option value="1m">1m</option><option value="5m">5m</option><option value="15m">15m</option><option value="1h" selected>1h</option><option value="4h">4h</option><option value="1d">1d</option></select>
|
||||||
<option value="1m">1m</option><option value="5m">5m</option><option value="15m">15m</option>
|
<select id="run-limit"><option value="100">100 bars</option><option value="200">200 bars</option><option value="500" selected>500 bars</option><option value="1000">1K bars</option><option value="2000">2K bars</option><option value="5000">5K bars</option></select>
|
||||||
<option value="1h" selected>1h</option><option value="4h">4h</option><option value="1d">1d</option>
|
<select id="run-coin"><option value="">auto</option><option value="BTC">BTC</option><option value="ETH">ETH</option><option value="SOL">SOL</option></select>
|
||||||
</select>
|
|
||||||
<select id="run-limit">
|
|
||||||
<option value="100">100 bars</option><option value="200">200 bars</option><option value="500" selected>500 bars</option>
|
|
||||||
<option value="1000">1K bars</option><option value="2000">2K bars</option><option value="5000">5K bars</option>
|
|
||||||
</select>
|
|
||||||
<select id="run-coin">
|
|
||||||
<option value="">auto</option><option value="BTC">BTC</option><option value="ETH">ETH</option><option value="SOL">SOL</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:4px">
|
<div style="display:flex;gap:4px">
|
||||||
<button class="btn btn-primary" onclick="runNewBacktest()" id="btn-run">▶ Run Backtest</button>
|
<button class="btn btn-primary" onclick="runNewBacktest()" id="btn-run">▶ Run Backtest</button>
|
||||||
@@ -184,7 +180,7 @@ select:focus{outline:none;border-color:#555}
|
|||||||
</div>
|
</div>
|
||||||
<div id="run-status"></div>
|
<div id="run-status"></div>
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:9px;color:#444;padding:2px 4px" id="result-count"></div>
|
<div style="font-size:9px;color:var(--text4);padding:2px 4px" id="result-count"></div>
|
||||||
<div id="results-list"><div class="loading">Loading...</div></div>
|
<div id="results-list"><div class="loading">Loading...</div></div>
|
||||||
<div class="pagination-row" id="pagination" style="display:none">
|
<div class="pagination-row" id="pagination" style="display:none">
|
||||||
<button class="nav-btn" id="btn-prev" onclick="loadPage(-1)" disabled>← Prev</button>
|
<button class="nav-btn" id="btn-prev" onclick="loadPage(-1)" disabled>← Prev</button>
|
||||||
@@ -194,77 +190,77 @@ select:focus{outline:none;border-color:#555}
|
|||||||
</div>
|
</div>
|
||||||
<div class="content" id="content">
|
<div class="content" id="content">
|
||||||
<div id="vbt-overview">
|
<div id="vbt-overview">
|
||||||
<div style="padding:10px 0"><span style="font-size:12px;color:#ddd;font-weight:600">OVERVIEW</span><span class="loading" id="overview-loading" style="display:inline;margin-left:10px;font-size:10px">Loading...</span></div>
|
<div style="padding:10px 0"><span style="font-size:12px;font-weight:600;color:var(--text3)">OVERVIEW</span><span class="loading" id="overview-loading" style="display:inline;margin-left:10px;font-size:10px">Loading...</span></div>
|
||||||
<div id="overview-content"></div>
|
<div id="overview-content"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const API='';
|
const API='',PAGE_SIZE=50;
|
||||||
const PAGE_SIZE=50;
|
let currentResult=null,currentFilename=null,currentOffset=0,currentTotal=0,runTimer=null;
|
||||||
let currentResult=null, currentFilename=null, currentOffset=0, currentTotal=0, runTimer=null;
|
|
||||||
|
|
||||||
function buildUrl(){let p=new URLSearchParams();const s=document.getElementById('filter-strategy').value,i=document.getElementById('filter-interval').value,a=document.getElementById('filter-asset').value,so=document.getElementById('filter-sort').value;p.set('limit',PAGE_SIZE);p.set('offset',currentOffset);if(s)p.set('strategy',s);if(i)p.set('interval',i);if(a)p.set('asset',a);p.set('sort',so);return`${API}/api/vbt/results?`+p;}
|
function toggleTheme(){
|
||||||
|
const html=document.documentElement;
|
||||||
|
html.classList.toggle('light');
|
||||||
|
const isLight=html.classList.contains('light');
|
||||||
|
document.getElementById('theme-btn').textContent=isLight?'☾':'☀';
|
||||||
|
localStorage.setItem('vbt-theme',isLight?'light':'dark');
|
||||||
|
// Re-render charts if visible
|
||||||
|
if(currentResult)renderCharts(currentResult);
|
||||||
|
}
|
||||||
|
(function(){
|
||||||
|
const saved=localStorage.getItem('vbt-theme');
|
||||||
|
if(saved==='light'){
|
||||||
|
document.documentElement.classList.add('light');
|
||||||
|
document.getElementById('theme-btn').textContent='☾';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
function buildUrl(){let p=new URLSearchParams();const s=document.getElementById('filter-strategy').value,i=document.getElementById('filter-interval').value,a=document.getElementById('filter-asset').value,so=document.getElementById('filter-sort').value;p.set('limit',PAGE_SIZE);p.set('offset',currentOffset);if(s)p.set('strategy',s);if(i)p.set('interval',i);if(a)p.set('asset',a);p.set('sort',so);return API+'/api/vbt/results?'+p;}
|
||||||
|
|
||||||
async function loadResults(){
|
async function loadResults(){
|
||||||
document.getElementById('results-list').innerHTML='<div class="loading">Loading...</div>';
|
document.getElementById('results-list').innerHTML='<div class="loading">Loading...</div>';
|
||||||
try{
|
try{
|
||||||
const resp=await fetch(buildUrl());const data=await resp.json();
|
const resp=await fetch(buildUrl());const data=await resp.json();
|
||||||
if(data&&Array.isArray(data.results)){
|
if(data&&Array.isArray(data.results)){renderResultsList(data.results,data.total||0,data.has_more??false);}
|
||||||
renderResultsList(data.results,data.total||0,data.has_more??false);
|
else if(Array.isArray(data)){renderResultsList(data,data.length,false);}
|
||||||
}else if(Array.isArray(data)){
|
else{throw new Error('Unexpected response');}
|
||||||
renderResultsList(data,data.length,false);
|
}catch(e){document.getElementById('results-list').innerHTML='<div class="error-box" style="padding:8px;font-size:10px">Failed to load results</div>';}
|
||||||
}else{
|
|
||||||
throw new Error('Unexpected response');
|
|
||||||
}
|
|
||||||
}catch(e){document.getElementById('results-list').innerHTML='<div class="error-box" style="padding:8px;font-size:10px">Failed to load results</div><div style="font-size:9px;color:#555;text-align:center;margin-top:4px">'+e+'</div>';}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderResultsList(results,total,hasMore){
|
function renderResultsList(results,total,hasMore){
|
||||||
currentTotal=total;
|
currentTotal=total;
|
||||||
const el=document.getElementById('results-list');
|
|
||||||
document.getElementById('result-count').textContent=total+' results';
|
document.getElementById('result-count').textContent=total+' results';
|
||||||
if(!results.length){el.innerHTML='<div style="padding:8px;color:#555;font-size:10px">No results</div>';document.getElementById('pagination').style.display='none';return;}
|
if(!results.length){document.getElementById('results-list').innerHTML='<div style="padding:8px;color:var(--text4);font-size:10px">No results</div>';document.getElementById('pagination').style.display='none';return;}
|
||||||
el.innerHTML=results.map((r,i)=>{
|
document.getElementById('results-list').innerHTML=results.map((r,i)=>{
|
||||||
const c=(r.sharpe||0)>0.5?'pos':((r.sharpe||0)<-0.5?'neg':'neu');
|
const c=(r.sharpe||0)>0.5?'pos':((r.sharpe||0)<-0.5?'neg':'neu');
|
||||||
const rc=(r.total_return_pct||0)>=0?'pos':'neg';
|
const rc=(r.total_return_pct||0)>=0?'pos':'neg';
|
||||||
const activeCls=r.filename===currentFilename?' active':'';
|
const activeCls=r.filename===currentFilename?' active':'';
|
||||||
return '<div class="result-item'+activeCls+'" onclick="selectFile(\''+r.filename+'\')" id="item-'+r.filename+'"><div class="name">'+r.strategy+'<span class="asset">'+(r.asset||'?')+'</span></div><div class="meta">'+(r.interval||'1h')+' · '+(r.total_trades||0)+'t · '+(r.n_bars||0)+'b · '+(r.generated_at||'').substring(0,10)+'</div><div class="stats"><span class="'+c+'">S'+(r.sharpe||0).toFixed(2)+'</span><span class="'+rc+'">'+(r.total_return_pct||0).toFixed(1)+'%</span><span class="neu">PF'+(r.profit_factor||0).toFixed(2)+'</span><span class="neu">WR'+(Math.round((r.win_rate||0)*100))+'%</span></div></div>';
|
return '<div class="result-item'+activeCls+'" onclick="selectFile(\''+r.filename+'\')" id="item-'+r.filename+'"><div class="name">'+r.strategy+'<span class="asset">'+(r.asset||'?')+'</span></div><div class="meta">'+(r.interval||'1h')+' · '+(r.total_trades||0)+'t · '+(r.n_bars||0)+'b · '+(r.generated_at||'').substring(0,10)+'</div><div class="stats"><span class="'+c+'">S'+(r.sharpe||0).toFixed(2)+'</span><span class="'+rc+'">'+(r.total_return_pct||0).toFixed(1)+'%</span><span class="neu">PF'+(r.profit_factor||0).toFixed(2)+'</span><span class="neu">WR'+Math.round((r.win_rate||0)*100)+'%</span></div></div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
document.getElementById('pagination').style.display=total>PAGE_SIZE?'flex':'none';
|
||||||
const pagination=document.getElementById('pagination');
|
|
||||||
if(total>PAGE_SIZE){
|
if(total>PAGE_SIZE){
|
||||||
pagination.style.display='flex';
|
|
||||||
document.getElementById('btn-prev').disabled=currentOffset===0;
|
document.getElementById('btn-prev').disabled=currentOffset===0;
|
||||||
document.getElementById('btn-next').disabled=!hasMore;
|
document.getElementById('btn-next').disabled=!hasMore;
|
||||||
const end=Math.min(currentOffset+PAGE_SIZE,total);
|
document.getElementById('page-info').textContent=(currentOffset+1)+'–'+Math.min(currentOffset+PAGE_SIZE,total)+' of '+total;
|
||||||
document.getElementById('page-info').textContent=(currentOffset+1)+'–'+end+' of '+total;
|
}
|
||||||
}else{pagination.style.display='none';}
|
|
||||||
document.getElementById('pagination').style.display=total>PAGE_SIZE?'flex':'none';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPage(dir){
|
function loadPage(dir){currentOffset=Math.max(0,currentOffset+dir*PAGE_SIZE);loadResults();}
|
||||||
currentOffset=Math.max(0,currentOffset+dir*PAGE_SIZE);
|
|
||||||
loadResults();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectFile(filename){
|
async function selectFile(filename){
|
||||||
document.querySelectorAll('.result-item').forEach(el=>el.classList.remove('active'));
|
document.querySelectorAll('.result-item').forEach(el=>el.classList.remove('active'));
|
||||||
const item=document.getElementById('item-'+filename);
|
document.getElementById('item-'+filename)?.classList.add('active');
|
||||||
if(item)item.classList.add('active');
|
currentFilename=filename;currentResult=null;
|
||||||
currentFilename=filename;
|
|
||||||
currentResult=null;
|
|
||||||
window.location.hash=filename;
|
window.location.hash=filename;
|
||||||
const overview=document.getElementById('vbt-overview');
|
document.getElementById('vbt-overview').style.display='none';
|
||||||
if(overview)overview.style.display='none';
|
document.getElementById('content').innerHTML='<div class="loading" style="padding:40px">Loading '+filename+'...</div>';
|
||||||
const content=document.getElementById('content');
|
|
||||||
content.innerHTML='<div class="loading" style="padding:40px">Loading '+filename+'...</div>';
|
|
||||||
try{
|
try{
|
||||||
const r=await(await fetch(API+'/api/vbt/result/'+encodeURIComponent(filename))).json();
|
const r=await(await fetch(API+'/api/vbt/result/'+encodeURIComponent(filename))).json();
|
||||||
if(r.error||!r.strategy){content.innerHTML='<div class="error-box"><h3>Error</h3><p style="font-size:11px;color:#999">'+r.error+'</p><button class="btn" onclick="selectFile(\''+filename+'\')">Retry</button></div>';return;}
|
if(r.error||!r.strategy){document.getElementById('content').innerHTML='<div class="error-box"><h3>Error</h3><p style="font-size:11px">'+r.error+'</p><button class="btn" onclick="selectFile(\''+filename+'\')">Retry</button></div>';return;}
|
||||||
currentResult=r;renderDetail(r);
|
currentResult=r;renderDetail(r);
|
||||||
}catch(e){content.innerHTML='<div class="error-box"><h3>Error</h3><p style="font-size:11px;color:#999">'+e+'</p><button class="btn" onclick="selectFile(\''+filename+'\')">Retry</button></div>';}
|
}catch(e){document.getElementById('content').innerHTML='<div class="error-box"><h3>Error</h3><p style="font-size:11px">'+e+'</p><button class="btn" onclick="selectFile(\''+filename+'\')">Retry</button></div>';}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDetail(r){
|
function renderDetail(r){
|
||||||
@@ -275,10 +271,6 @@ function renderDetail(r){
|
|||||||
const calmar=r.calmar||((dd)?(ret/Math.abs(dd)):0);
|
const calmar=r.calmar||((dd)?(ret/Math.abs(dd)):0);
|
||||||
const expec=r.expectancy??(tr?(r.pnl??0)/tr:0);
|
const expec=r.expectancy??(tr?(r.pnl??0)/tr:0);
|
||||||
|
|
||||||
let avgDur='';
|
|
||||||
const trades=r.trades||[];
|
|
||||||
if(trades.length){const durations=trades.map(t=>String(t.duration||'')).filter(d=>d);if(durations.length)avgDur=durations[0];}
|
|
||||||
|
|
||||||
let params_html='';
|
let params_html='';
|
||||||
for(const[k,v]of Object.entries(params)){
|
for(const[k,v]of Object.entries(params)){
|
||||||
let cls='neu';
|
let cls='neu';
|
||||||
@@ -287,26 +279,24 @@ function renderDetail(r){
|
|||||||
}
|
}
|
||||||
|
|
||||||
let trades_html='';
|
let trades_html='';
|
||||||
|
const trades=r.trades||[];
|
||||||
if(trades.length){
|
if(trades.length){
|
||||||
const rows=trades.map(t=>{
|
const rows=trades.map(t=>{
|
||||||
const pnl_net=Number(t.pnl_net??t.pnl??0);
|
const pnl_net=Number(t.pnl_net??t.pnl??0);
|
||||||
const fee=Number(t.fee??0);
|
const fee=Number(t.fee??0);
|
||||||
const pnlCls=pnl_net>=0?'pnl-pos':'pnl-neg';
|
const pnlCls=pnl_net>=0?'pnl-pos':'pnl-neg';
|
||||||
const assetBadge = t.asset ? '<span style="font-size:9px;color:#03A9F4;background:#0d1f2b;border:1px solid #1a3a4a;border-radius:2px;padding:0 4px;margin-left:4px">'+t.asset+'</span>' : '';
|
const assetBadge=t.asset?'<span style="font-size:9px;color:var(--blue);background:var(--bg3);border:1px solid var(--border);border-radius:2px;padding:0 4px;margin-left:4px">'+t.asset+'</span>':'';
|
||||||
return '<tr><td style="color:#777;font-size:10px">'+String(t.time||'').substring(0,19)+'</td><td class="'+(String(t.side||'').includes('BUY')?'pos':'neg')+'">'+String(t.side||'')+assetBadge+'</td><td>'+Number(t.size||0).toFixed(6)+'</td><td>$'+Number(t.entry_px||0).toFixed(1)+'</td><td>$'+Number(t.exit_px||0).toFixed(1)+'</td><td class="neg" style="font-size:10px">$'+fee.toFixed(6)+'</td><td class="'+pnlCls+'">$'+pnl_net.toFixed(4)+'</td><td style="font-size:10px">'+String(t.duration||'')+'</td></tr>';
|
return '<tr><td style="font-size:10px;color:var(--text4)">'+String(t.time||'').substring(0,19)+'</td><td class="'+(String(t.side||'').includes('BUY')?'pos':'neg')+'">'+String(t.side||'')+assetBadge+'</td><td>'+Number(t.size||0).toFixed(6)+'</td><td>$'+Number(t.entry_px||0).toFixed(1)+'</td><td>$'+Number(t.exit_px||0).toFixed(1)+'</td><td class="neg" style="font-size:10px">$'+fee.toFixed(6)+'</td><td class="'+pnlCls+'">$'+pnl_net.toFixed(4)+'</td><td style="font-size:10px">'+String(t.duration||'')+'</td></tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
const feeRate = trades[0]?.fee_rate_pct ? ' · '+trades[0].fee_rate_pct+'%' : '';
|
const feeRate=trades[0]?.fee_rate_pct?' · '+trades[0].fee_rate_pct+'%':'';
|
||||||
trades_html='<div class="chart-box full"><div style="display:flex;justify-content:space-between;align-items:center"><h4>Trade Log ('+trades.length+' trades'+feeRate+')</h4></div><div class="trade-scroll"><table class="trades-table"><thead><tr><th>Time</th><th>Side</th><th>Size</th><th>Entry</th><th>Exit</th><th>Fee</th><th>PnL (net)</th><th>Duration</th></tr></thead><tbody>'+rows+'</tbody></table></div></div>';
|
trades_html='<div class="chart-box full"><div style="display:flex;justify-content:space-between;align-items:center"><h4>Trade Log ('+trades.length+' trades'+feeRate+')</h4></div><div class="trade-scroll"><table class="trades-table"><thead><tr><th>Time</th><th>Side</th><th>Size</th><th>Entry</th><th>Exit</th><th>Fee</th><th>PnL (net)</th><th>Duration</th></tr></thead><tbody>'+rows+'</tbody></table></div></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('content').innerHTML=
|
document.getElementById('content').innerHTML=
|
||||||
'<div style="margin-bottom:8px"><button class="btn" onclick="showOverview()">← Overview</button></div>'+
|
'<div style="margin-bottom:8px"><button class="btn" onclick="showOverview()">← Overview</button></div>'+
|
||||||
'<h2>'+r.strategy+' <span class="badge" style="font-size:10px;background:#0d1f2b;color:#03A9F4;border:1px solid #1a3a4a">'+asset+'</span></h2>'+
|
'<h2>'+r.strategy+' <span class="badge" style="font-size:10px;background:#0d1f2b;color:var(--blue);border:1px solid #1a3a4a">'+asset+'</span></h2>'+
|
||||||
'<div class="sub"><span>'+(r.interval||'1h')+'</span><span>'+nb+' bars</span><span>'+tr+' trades</span><span>'+(r.generated_at||'').substring(0,19)+'</span>'+
|
'<div class="sub"><span>'+(r.interval||'1h')+'</span><span>'+nb+' bars</span><span>'+tr+' trades</span><span>'+(r.generated_at||'').substring(0,19)+'</span>'+
|
||||||
'<div class="export-row">'+
|
'<div class="export-row"><button class="btn" onclick="exportJSON()">JSON</button><button class="btn" onclick="exportCSV()">CSV</button></div></div>'+
|
||||||
'<button class="btn" onclick="exportJSON()" title="Download full result as JSON">JSON</button>'+
|
|
||||||
'<button class="btn" onclick="exportCSV()" title="Download trades as CSV">CSV</button>'+
|
|
||||||
'</div></div>'+
|
|
||||||
'<div class="metrics-grid">'+
|
'<div class="metrics-grid">'+
|
||||||
'<div class="metric"><div class="label">Total Return</div><div class="value '+(ret>=0?'pos':'neg')+'">'+ret.toFixed(2)+'%</div></div>'+
|
'<div class="metric"><div class="label">Total Return</div><div class="value '+(ret>=0?'pos':'neg')+'">'+ret.toFixed(2)+'%</div></div>'+
|
||||||
'<div class="metric"><div class="label">Sharpe</div><div class="value '+(sh>=0?'pos':'neg')+'">'+sh.toFixed(2)+'</div></div>'+
|
'<div class="metric"><div class="label">Sharpe</div><div class="value '+(sh>=0?'pos':'neg')+'">'+sh.toFixed(2)+'</div></div>'+
|
||||||
@@ -320,13 +310,8 @@ function renderDetail(r){
|
|||||||
'<div class="metric"><div class="label">End Equity</div><div class="value neu">$'+parseInt(eq).toLocaleString()+'</div></div>'+
|
'<div class="metric"><div class="label">End Equity</div><div class="value neu">$'+parseInt(eq).toLocaleString()+'</div></div>'+
|
||||||
'</div>'+
|
'</div>'+
|
||||||
'<div class="chart-box full"><h4>Strategy Parameters</h4><div class="params-grid">'+params_html+'</div></div>'+
|
'<div class="chart-box full"><h4>Strategy Parameters</h4><div class="params-grid">'+params_html+'</div></div>'+
|
||||||
'<div class="chart-row">'+
|
'<div class="chart-row"><div class="chart-box full"><h4>Equity Curve</h4><div id="chart-eq" style="height:300px"></div></div></div>'+
|
||||||
'<div class="chart-box full"><h4>Equity Curve</h4><div id="chart-eq" style="height:300px"></div></div>'+
|
'<div class="chart-row"><div class="chart-box"><h4>Drawdown</h4><div id="chart-dd" style="height:250px"></div></div><div class="chart-box"><h4>Period Returns</h4><div id="chart-ret" style="height:250px"></div></div></div>'+
|
||||||
'</div>'+
|
|
||||||
'<div class="chart-row">'+
|
|
||||||
'<div class="chart-box"><h4>Drawdown</h4><div id="chart-dd" style="height:250px"></div></div>'+
|
|
||||||
'<div class="chart-box"><h4>Period Returns</h4><div id="chart-ret" style="height:250px"></div></div>'+
|
|
||||||
'</div>'+
|
|
||||||
trades_html;
|
trades_html;
|
||||||
renderCharts(r);
|
renderCharts(r);
|
||||||
}
|
}
|
||||||
@@ -334,28 +319,32 @@ function renderDetail(r){
|
|||||||
function renderCharts(r){
|
function renderCharts(r){
|
||||||
const ec=r.equity_curve||[];
|
const ec=r.equity_curve||[];
|
||||||
if(!ec.length)return;
|
if(!ec.length)return;
|
||||||
|
const isLight=document.documentElement.classList.contains('light');
|
||||||
const times=ec.map(p=>p.t),values=ec.map(p=>p.v);
|
const times=ec.map(p=>p.t),values=ec.map(p=>p.v);
|
||||||
Plotly.newPlot('chart-eq',[{x:times,y:values,type:'scatter',mode:'lines',line:{color:'#03A9F4',width:1.2},fill:'tozeroy',fillcolor:'rgba(3,169,244,0.06)'}],{margin:{t:4,r:12,b:28,l:65},height:300,paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',xaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9}},yaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9},tickprefix:'$'},showlegend:false},{responsive:true,displayModeBar:false});
|
const lineC=isLight?'#0288d1':'#03A9F4';
|
||||||
|
const fillC=isLight?'rgba(2,136,209,0.08)':'rgba(3,169,244,0.06)';
|
||||||
|
const gridC=isLight?'#e8e8e8':'#1a1a1a';
|
||||||
|
const tickC=isLight?'#999':'#444';
|
||||||
|
Plotly.newPlot('chart-eq',[{x:times,y:values,type:'scatter',mode:'lines',line:{color:lineC,width:1.2},fill:'tozeroy',fillcolor:fillC}],{margin:{t:4,r:12,b:28,l:65},height:300,paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',xaxis:{gridcolor:gridC,tickfont:{color:tickC,size:9}},yaxis:{gridcolor:gridC,tickfont:{color:tickC,size:9},tickprefix:'$'},showlegend:false},{responsive:true,displayModeBar:false});
|
||||||
const ddData=r.drawdown_curve||[];
|
const ddData=r.drawdown_curve||[];
|
||||||
if(ddData.length){Plotly.newPlot('chart-dd',[{x:times,y:ddData,type:'scatter',mode:'none',fill:'tozeroy',fillcolor:'rgba(255,82,82,0.10)',line:{color:'#FF5252',width:0.8}}],{margin:{t:4,r:12,b:28,l:55},height:250,paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',xaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9}},yaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9},ticksuffix:'%'},showlegend:false},{responsive:true,displayModeBar:false});}else{const peak=values.reduce((a,v,i)=>(a.push(i?Math.max(a[i-1],v):v),a),[]);const dd=values.map((v,i)=>i?-(peak[i]-v)/peak[i]*100:0);Plotly.newPlot('chart-dd',[{x:times,y:dd,type:'scatter',mode:'none',fill:'tozeroy',fillcolor:'rgba(255,82,82,0.10)',line:{color:'#FF5252',width:0.8}}],{margin:{t:4,r:12,b:28,l:55},height:250,paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',xaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9}},yaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9},ticksuffix:'%'},showlegend:false},{responsive:true,displayModeBar:false});}
|
if(ddData.length){Plotly.newPlot('chart-dd',[{x:times,y:ddData,type:'scatter',mode:'none',fill:'tozeroy',fillcolor:isLight?'rgba(200,0,0,0.08)':'rgba(255,82,82,0.10)',line:{color:'#FF5252',width:0.8}}],{margin:{t:4,r:12,b:28,l:55},height:250,paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',xaxis:{gridcolor:gridC,tickfont:{color:tickC,size:9}},yaxis:{gridcolor:gridC,tickfont:{color:tickC,size:9},ticksuffix:'%'},showlegend:false},{responsive:true,displayModeBar:false});}
|
||||||
if(values.length>1){const rets=values.slice(1).map((v,i)=>(v-values[i])/values[i]*100);Plotly.newPlot('chart-ret',[{x:rets,type:'histogram',nbinsx:40,marker:{color:'#03A9F4',opacity:0.6,line:{color:'#111',width:1}}}],{margin:{t:4,r:12,b:28,l:45},height:250,paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',xaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9},ticksuffix:'%'},yaxis:{gridcolor:'#1a1a1a',tickfont:{color:'#444',size:9}},showlegend:false,bargap:0.02},{responsive:true,displayModeBar:false});}
|
else{const peak=values.reduce((a,v,i)=>(a.push(i?Math.max(a[i-1],v):v),a),[]);const dd=values.map((v,i)=>i?-(peak[i]-v)/peak[i]*100:0);Plotly.newPlot('chart-dd',[{x:times,y:dd,type:'scatter',mode:'none',fill:'tozeroy',fillcolor:isLight?'rgba(200,0,0,0.08)':'rgba(255,82,82,0.10)',line:{color:'#FF5252',width:0.8}}],{margin:{t:4,r:12,b:28,l:55},height:250,paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',xaxis:{gridcolor:gridC,tickfont:{color:tickC,size:9}},yaxis:{gridcolor:gridC,tickfont:{color:tickC,size:9},ticksuffix:'%'},showlegend:false},{responsive:true,displayModeBar:false});}
|
||||||
|
if(values.length>1){const rets=values.slice(1).map((v,i)=>(v-values[i])/values[i]*100);Plotly.newPlot('chart-ret',[{x:rets,type:'histogram',nbinsx:40,marker:{color:lineC,opacity:0.6,line:{color:isLight?'#eee':'#111',width:1}}}],{margin:{t:4,r:12,b:28,l:45},height:250,paper_bgcolor:'rgba(0,0,0,0)',plot_bgcolor:'rgba(0,0,0,0)',xaxis:{gridcolor:gridC,tickfont:{color:tickC,size:9},ticksuffix:'%'},yaxis:{gridcolor:gridC,tickfont:{color:tickC,size:9}},showlegend:false,bargap:0.02},{responsive:true,displayModeBar:false});}
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportJSON(){if(!currentResult)return;const blob=new Blob([JSON.stringify(currentResult,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=(currentFilename||'backtest')+'.json';a.click();}
|
function exportJSON(){if(!currentResult)return;const blob=new Blob([JSON.stringify(currentResult,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=(currentFilename||'backtest')+'.json';a.click();}
|
||||||
|
|
||||||
function exportCSV(){if(!currentFilename)return;const a=document.createElement('a');a.href=API+'/api/vbt/result/'+encodeURIComponent(currentFilename)+'/csv';a.download=currentFilename.replace('.json','')+'_trades.csv';a.click();}
|
function exportCSV(){if(!currentFilename)return;const a=document.createElement('a');a.href=API+'/api/vbt/result/'+encodeURIComponent(currentFilename)+'/csv';a.download=currentFilename.replace('.json','')+'_trades.csv';a.click();}
|
||||||
|
|
||||||
async function recalcIfSelected(){
|
async function recalcIfSelected(){
|
||||||
if(!currentFilename)return;
|
if(!currentFilename)return;
|
||||||
const feeTier=document.getElementById('fee-tier').value;
|
const feeTier=document.getElementById('fee-tier').value;
|
||||||
const staking=document.getElementById('staking-tier').value;
|
const staking=document.getElementById('staking-tier').value;
|
||||||
const makerRebate=0;
|
|
||||||
try{
|
try{
|
||||||
const url=API+'/api/vbt/result/'+encodeURIComponent(currentFilename)+'/recalc?fee_tier='+feeTier+'&staking_tier='+staking+'&maker_rebate='+makerRebate;
|
const url=API+'/api/vbt/result/'+encodeURIComponent(currentFilename)+'/recalc?fee_tier='+feeTier+'&staking_tier='+staking;
|
||||||
const r=await(await fetch(url)).json();
|
const r=await(await fetch(url)).json();
|
||||||
if(r.error){return;}
|
if(r.error)return;
|
||||||
currentResult=r;renderDetail(r);
|
currentResult=r;renderDetail(r);
|
||||||
}catch(e){/* ignore */}
|
}catch(e){}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runNewBacktest(){
|
async function runNewBacktest(){
|
||||||
@@ -365,27 +354,21 @@ async function runNewBacktest(){
|
|||||||
const coin=document.getElementById('run-coin').value;
|
const coin=document.getElementById('run-coin').value;
|
||||||
const feeTier=document.getElementById('fee-tier').value;
|
const feeTier=document.getElementById('fee-tier').value;
|
||||||
const staking=document.getElementById('staking-tier').value;
|
const staking=document.getElementById('staking-tier').value;
|
||||||
const btn=document.getElementById('btn-run');
|
const btn=document.getElementById('btn-run'),status=document.getElementById('run-status');
|
||||||
const status=document.getElementById('run-status');
|
|
||||||
const orig=btn.textContent;btn.textContent='⏳ Running...';btn.disabled=true;
|
const orig=btn.textContent;btn.textContent='⏳ Running...';btn.disabled=true;
|
||||||
status.innerHTML='<div class="run-status">Running '+strat+' '+interval+'...</div><div class="progress-bar" style="width:0%" id="progress-bar"></div>';
|
status.innerHTML='<div class="run-status">Running '+strat+' '+interval+'...</div><div class="progress-bar" style="width:0%" id="progress-bar"></div>';
|
||||||
const startTime=Date.now();
|
const startTime=Date.now();
|
||||||
runTimer=setInterval(()=>{
|
runTimer=setInterval(()=>{const e=Math.round((Date.now()-startTime)/1000);status.innerHTML='<div class="run-status">Running '+strat+' '+interval+'... '+e+'s</div><div class="progress-bar" style="width:'+Math.min(e*10,95)+'%"></div>';},1000);
|
||||||
const elapsed=Math.round((Date.now()-startTime)/1000);
|
|
||||||
status.innerHTML='<div class="run-status">Running '+strat+' '+interval+'... '+elapsed+'s</div><div class="progress-bar" style="width:'+Math.min(elapsed*10,95)+'%"></div>';
|
|
||||||
},1000);
|
|
||||||
try{
|
try{
|
||||||
let url=API+'/api/vbt/run?strategy='+strat+'&interval='+interval+'&limit='+limit+'&fee_tier='+feeTier+'&staking_tier='+staking;
|
let url=API+'/api/vbt/run?strategy='+strat+'&interval='+interval+'&limit='+limit+'&fee_tier='+feeTier+'&staking_tier='+staking;
|
||||||
if(coin)url+='&coin='+coin;
|
if(coin)url+='&coin='+coin;
|
||||||
const resp=await fetch(url);
|
const resp=await fetch(url);const data=await resp.json();
|
||||||
const data=await resp.json();
|
|
||||||
clearInterval(runTimer);runTimer=null;
|
clearInterval(runTimer);runTimer=null;
|
||||||
if(data.error){status.innerHTML='<div style="font-size:10px;color:#FF5252;padding:4px 0">Error: '+data.error+'</div>';btn.textContent=orig;btn.disabled=false;return;}
|
if(data.error){status.innerHTML='<div style="font-size:10px;color:var(--red);padding:4px 0">Error: '+data.error+'</div>';btn.textContent=orig;btn.disabled=false;return;}
|
||||||
currentResult=data;currentFilename=data.filename;
|
currentResult=data;currentFilename=data.filename;
|
||||||
renderDetail(data);
|
renderDetail(data);currentOffset=0;loadResults();
|
||||||
currentOffset=0;loadResults();
|
status.innerHTML='<div style="font-size:10px;color:var(--green);padding:4px 0">Done in '+Math.round((Date.now()-startTime)/1000)+'s</div>';
|
||||||
status.innerHTML='<div style="font-size:10px;color:#4CAF50;padding:4px 0">Done in '+Math.round((Date.now()-startTime)/1000)+'s</div>';
|
}catch(e){clearInterval(runTimer);runTimer=null;status.innerHTML='<div style="font-size:10px;color:var(--red);padding:4px 0">Failed: '+e.message+'</div>';}
|
||||||
}catch(e){clearInterval(runTimer);runTimer=null;status.innerHTML='<div style="font-size:10px;color:#FF5252;padding:4px 0">Failed: '+e.message+'</div>';}
|
|
||||||
btn.textContent=orig;btn.disabled=false;
|
btn.textContent=orig;btn.disabled=false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,9 +383,9 @@ async function loadSummary(){
|
|||||||
try{
|
try{
|
||||||
const resp=await fetch(API+'/api/vbt/summary');
|
const resp=await fetch(API+'/api/vbt/summary');
|
||||||
const d=await resp.json();
|
const d=await resp.json();
|
||||||
if(d.error){document.getElementById('overview-content').innerHTML='<div style="color:#555;padding:10px">'+d.error+'</div>';return;}
|
if(d.error){document.getElementById('overview-content').innerHTML='<div style="padding:10px;color:var(--text4)">'+d.error+'</div>';return;}
|
||||||
renderSummary(d);
|
renderSummary(d);
|
||||||
}catch(e){document.getElementById('overview-content').innerHTML='<div style="color:#FF5252;padding:10px">Failed to load summary</div>';}
|
}catch(e){document.getElementById('overview-content').innerHTML='<div style="padding:10px;color:var(--red)">Failed to load summary</div>';}
|
||||||
document.getElementById('overview-loading').style.display='none';
|
document.getElementById('overview-loading').style.display='none';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,25 +394,18 @@ function renderSummary(d){
|
|||||||
const heatmap=d.heatmap_best_sharpe||{};
|
const heatmap=d.heatmap_best_sharpe||{};
|
||||||
const intervals=d.intervals||[];
|
const intervals=d.intervals||[];
|
||||||
const stratNames=Object.keys(strategies).sort();
|
const stratNames=Object.keys(strategies).sort();
|
||||||
|
|
||||||
// Strategy cards
|
|
||||||
let cards='<div class="strat-summary">';
|
let cards='<div class="strat-summary">';
|
||||||
for(const s of stratNames){
|
for(const s of stratNames){
|
||||||
const info=strategies[s];
|
const info=strategies[s];
|
||||||
const sh=info.best_sharpe||0;
|
const sh=info.best_sharpe||0;
|
||||||
const shCls=sh>0.5?'pos':(sh<-0.5?'neg':'neu');
|
|
||||||
const retCls=info.best_return>=0?'pos':'neg';
|
|
||||||
cards+='<div class="strat-card" onclick="document.getElementById(\'filter-strategy\').value=\''+s+'\';applyFilter()"><div class="strat-name">'+s+'</div>'+
|
cards+='<div class="strat-card" onclick="document.getElementById(\'filter-strategy\').value=\''+s+'\';applyFilter()"><div class="strat-name">'+s+'</div>'+
|
||||||
'<div class="strat-row"><span>Best Sharpe</span><span class="val '+shCls+'">'+sh.toFixed(2)+'</span></div>'+
|
'<div class="strat-row"><span>Best Sharpe</span><span class="val '+(sh>0.5?'pos':(sh<-0.5?'neg':'neu'))+'">'+sh.toFixed(2)+'</span></div>'+
|
||||||
'<div class="strat-row"><span>Best Return</span><span class="val '+retCls+'">'+(info.best_return||0).toFixed(1)+'%</span></div>'+
|
'<div class="strat-row"><span>Best Return</span><span class="val '+(info.best_return>=0?'pos':'neg')+'">'+(info.best_return||0).toFixed(1)+'%</span></div>'+
|
||||||
'<div class="strat-row"><span>Best Combo</span><span class="val">'+info.top_combo.interval+' '+info.top_combo.limit+'b</span></div>'+
|
'<div class="strat-row"><span>Best Combo</span><span class="val">'+info.top_combo.interval+' '+info.top_combo.limit+'b</span></div>'+
|
||||||
'<div class="strat-row"><span>+Sharpe %</span><span class="val">'+(info.positive_sharpe_pct||0).toFixed(0)+'%</span></div>'+
|
'<div class="strat-row"><span>+Sharpe %</span><span class="val">'+(info.positive_sharpe_pct||0).toFixed(0)+'%</span></div>'+
|
||||||
'<div class="strat-row"><span>Runs</span><span class="val">'+info.count+'</span></div>'+
|
'<div class="strat-row"><span>Runs</span><span class="val">'+info.count+'</span></div></div>';
|
||||||
'</div>';
|
|
||||||
}
|
}
|
||||||
cards+='</div>';
|
cards+='</div>';
|
||||||
|
|
||||||
// Heatmap
|
|
||||||
let hmap='<div class="chart-box full"><h4>Strategy × Interval Heatmap (Best Sharpe)</h4><div style="overflow-x:auto"><table class="heatmap-table"><thead><tr><th></th>';
|
let hmap='<div class="chart-box full"><h4>Strategy × Interval Heatmap (Best Sharpe)</h4><div style="overflow-x:auto"><table class="heatmap-table"><thead><tr><th></th>';
|
||||||
for(const iv of intervals)hmap+='<th>'+iv+'</th>';
|
for(const iv of intervals)hmap+='<th>'+iv+'</th>';
|
||||||
hmap+='</tr></thead><tbody>';
|
hmap+='</tr></thead><tbody>';
|
||||||
@@ -438,38 +414,25 @@ function renderSummary(d){
|
|||||||
for(const iv of intervals){
|
for(const iv of intervals){
|
||||||
const cell=(heatmap[s]||{})[iv]||{};
|
const cell=(heatmap[s]||{})[iv]||{};
|
||||||
const sh=cell.sharpe||0;
|
const sh=cell.sharpe||0;
|
||||||
let bg='#111',clr='#555';
|
let bg='var(--bg2)',clr='var(--text4)';
|
||||||
if(sh>2){bg='#0a2a0a';clr='#00e676'}
|
if(sh>2){bg='#0a2a0a';clr='#00e676'}
|
||||||
else if(sh>0.5){bg='#0a1a0a';clr='#4caf50'}
|
else if(sh>0.5){bg='#0a1a0a';clr='#4caf50'}
|
||||||
else if(sh>0){bg='#1a1a0a';clr='#aaa'}
|
else if(sh>0){bg='#1a1a0a';clr='#aaa'}
|
||||||
else if(sh>-0.5){bg='#1a0a0a';clr='#aa5555'}
|
else if(sh>-0.5){bg='#1a0a0a';clr='#cc7777'}
|
||||||
else {bg='#2a0a0a';clr='#ff1744'}
|
else{bg='#2a0a0a';clr='#ff1744'}
|
||||||
hmap+='<td class="heatmap-cell" style="background:'+bg+';color:'+clr+'" title="'+s+' '+iv+': S='+sh.toFixed(2)+' ret='+(cell.return_pct||0).toFixed(1)+'% '+cell.trades+'t" onclick="document.getElementById(\'filter-strategy\').value=\''+s+'\';document.getElementById(\'filter-interval\').value=\''+iv+'\';selectFile(\''+(cell.filename||(s+'_BTC_'+iv+'_500_'+(new Date().toISOString().slice(0,10).replace(/-/g,''))+'.json'))+'\')">'+sh.toFixed(1)+'</td>';
|
hmap+='<td class="heatmap-cell" style="background:'+bg+';color:'+clr+'" title="'+s+' '+iv+': S='+sh.toFixed(2)+' ret='+(cell.return_pct||0).toFixed(1)+'% '+cell.trades+'t">'+sh.toFixed(1)+'</td>';
|
||||||
}
|
}
|
||||||
hmap+='</tr>';
|
hmap+='</tr>';
|
||||||
}
|
}
|
||||||
hmap+='</tbody></table></div></div>';
|
hmap+='</tbody></table></div></div>';
|
||||||
|
document.getElementById('overview-content').innerHTML='<div style="font-size:10px;color:var(--text4);margin-bottom:6px">'+d.total_results+' results across '+d.strategy_count+' strategies</div>'+cards+hmap;
|
||||||
document.getElementById('overview-content').innerHTML=
|
|
||||||
'<div style="font-size:10px;color:#555;margin-bottom:6px">'+d.total_results+' results across '+d.strategy_count+' strategies</div>'+
|
|
||||||
cards+hmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreFromHash(){
|
function restoreFromHash(){const hash=window.location.hash.replace('#','');if(hash&&hash.endsWith('.json'))selectFile(hash);}
|
||||||
const hash=window.location.hash.replace('#','');
|
function showOverview(){currentFilename=null;currentResult=null;window.location.hash='';document.getElementById('vbt-overview').style.display='block';document.getElementById('content').innerHTML='<div id="vbt-overview"><div style="padding:10px 0"><span style="font-size:12px;font-weight:600;color:var(--text3)">OVERVIEW</span></div><div id="overview-content"></div></div>';document.querySelectorAll('.result-item').forEach(el=>el.classList.remove('active'));loadSummary();}
|
||||||
if(hash&&hash.endsWith('.json')){selectFile(hash);}
|
|
||||||
}
|
|
||||||
function showOverview(){
|
|
||||||
currentFilename=null;currentResult=null;
|
|
||||||
window.location.hash='';
|
|
||||||
document.getElementById('vbt-overview').style.display='block';
|
|
||||||
document.getElementById('content').innerHTML='<div id="vbt-overview"><div style="padding:10px 0"><span style="font-size:12px;color:#ddd;font-weight:600">OVERVIEW</span></div><div id="overview-content"></div></div>';
|
|
||||||
document.querySelectorAll('.result-item').forEach(el=>el.classList.remove('active'));
|
|
||||||
loadSummary();
|
|
||||||
}
|
|
||||||
window.addEventListener('hashchange',restoreFromHash);
|
window.addEventListener('hashchange',restoreFromHash);
|
||||||
loadSummary();
|
loadSummary();loadResults();
|
||||||
loadResults();
|
|
||||||
setTimeout(restoreFromHash,100);
|
setTimeout(restoreFromHash,100);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user