feat: VectorBT results dashboard with Plotly charts
Dashboard (dashboard/):
- New /api/vbt/results — list VBT backtest results with full metrics
- New /api/vbt/result/{file} — load result + equity curve (auto-decimated >500pts)
- New /api/vbt/run — run backtests on-demand from the UI
- New /api/vbt/sweep — parameter sweep as heatmap data
- New /api/vbt/strategies — list available strategy keys
- New /vbt — interactive HTML dashboard (Plotly.js):
- Equity curve chart with area fill
- Drawdown waterfall chart
- Returns distribution histogram
- Metric cards: Sharpe, Sortino, max DD, win rate, profit factor
- Strategy filter sidebar
- One-click backtest runner
- Fix BACKTEST_DIR auto-detection for local/dev paths
API verified: all 5 endpoints tested against live data
This commit is contained in:
+141
-9
@@ -27,20 +27,27 @@ from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
# Fix BACKTEST_DIR — auto-detect local path if deployed dir doesn't exist
|
||||
_default_results = str(Path(__file__).resolve().parent.parent / "backtests" / "results")
|
||||
BACKTEST_DIR = _default_results if os.path.isdir(_default_results) else "/home/debian/ftdt-quant-lab/backtests/results"
|
||||
HISTORICAL_DIR = BACKTEST_DIR + "/historical" if os.path.isdir(BACKTEST_DIR + "/historical") else BACKTEST_DIR
|
||||
|
||||
from config.fee_tiers import get_perp_fees, PERPS_TIERS, STAKING_TIERS, STRATEGY_FEE_MODELS
|
||||
from common.risk import risk_summary
|
||||
from strategies.quant_report import compute_quant_report
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# Memory guard: cap RSS at 512MB, GC-aggressive at 256MB
|
||||
# Memory guard: check RSS via /proc, force GC at 256MB,
|
||||
# log warning at 384MB, hard exit at 512MB.
|
||||
# RLIMIT_AS disabled — Python heap needs virtual headroom.
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
import resource, gc, signal
|
||||
import gc, os as _os
|
||||
|
||||
MEM_SOFT_LIMIT = 256 * 1024 * 1024 # 256 MB — force GC
|
||||
MEM_WARN_LIMIT = 384 * 1024 * 1024 # 384 MB — log warning
|
||||
MEM_HARD_LIMIT = 512 * 1024 * 1024 # 512 MB — terminate
|
||||
|
||||
resource.setrlimit(resource.RLIMIT_AS, (MEM_HARD_LIMIT, MEM_HARD_LIMIT))
|
||||
|
||||
def check_memory():
|
||||
"""Check RSS, force GC if over soft limit, raise if over hard limit."""
|
||||
try:
|
||||
@@ -51,7 +58,7 @@ def check_memory():
|
||||
rss = rss_kb * 1024
|
||||
if rss > MEM_HARD_LIMIT:
|
||||
print(f"[CRIT] RSS {rss_kb // 1024}MB > 512MB — exiting", flush=True)
|
||||
os._exit(1)
|
||||
_os._exit(1)
|
||||
if rss > MEM_SOFT_LIMIT:
|
||||
gc.collect()
|
||||
gc.collect()
|
||||
@@ -66,11 +73,8 @@ import uvicorn
|
||||
|
||||
METRICS_FILE = "/tmp/ftdt-metrics.json"
|
||||
PAPER_METRICS_FILE = "/tmp/ftdt-paper-metrics.json"
|
||||
BACKTEST_DIR = "/home/debian/ftdt-quant-lab/backtests/results"
|
||||
HISTORICAL_DIR = "/home/debian/ftdt-quant-lab/backtests/results/historical"
|
||||
STATIC_DIR = Path(__file__).parent / "static"
|
||||
|
||||
# Ensure backtest dir exists
|
||||
os.makedirs(BACKTEST_DIR, exist_ok=True)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
@@ -235,7 +239,10 @@ async def list_backtests():
|
||||
|
||||
@app.get("/api/backtest/{name}")
|
||||
async def get_backtest(name: str):
|
||||
"""Get full backtest result data."""
|
||||
"""Get full backtest result data — checks historical dir first."""
|
||||
# Try historical subdirectory first (where dashboard saves backtests)
|
||||
fpath = os.path.join(HISTORICAL_DIR, f"{name}.json")
|
||||
if not os.path.exists(fpath):
|
||||
fpath = os.path.join(BACKTEST_DIR, f"{name}.json")
|
||||
if os.path.exists(fpath):
|
||||
with open(fpath) as f:
|
||||
@@ -451,6 +458,126 @@ async def get_risk_metrics():
|
||||
"correlation_matrix": corr,
|
||||
})
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# VBT Dashboard API — VectorBT backtest results browser
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
@app.get("/api/vbt/results")
|
||||
async def list_vbt_results(strategy: str = "", limit: int = 50):
|
||||
"""List VectorBT backtest results with full metrics."""
|
||||
results = []
|
||||
for d in [BACKTEST_DIR, HISTORICAL_DIR]:
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
for fname in sorted(os.listdir(d), reverse=True):
|
||||
if not fname.endswith(".json"):
|
||||
continue
|
||||
if strategy and strategy not in fname:
|
||||
continue
|
||||
fpath = os.path.join(d, fname)
|
||||
try:
|
||||
with open(fpath) as f:
|
||||
data = json.load(f)
|
||||
results.append({
|
||||
"filename": fname,
|
||||
"strategy": data.get("strategy", "unknown"),
|
||||
"engine": data.get("engine", "vectorbt"),
|
||||
"interval": data.get("interval", "1h"),
|
||||
"sharpe": data.get("sharpe", 0),
|
||||
"sortino": data.get("sortino", 0),
|
||||
"total_return_pct": data.get("total_return_pct", 0),
|
||||
"max_drawdown_pct": data.get("max_drawdown_pct", 0),
|
||||
"win_rate": data.get("win_rate", 0),
|
||||
"profit_factor": data.get("profit_factor", 0),
|
||||
"total_trades": data.get("total_trades", 0),
|
||||
"n_bars": data.get("n_bars", 0),
|
||||
"generated_at": data.get("generated_at", ""),
|
||||
"has_equity_curve": bool(data.get("equity_curve")),
|
||||
})
|
||||
except (json.JSONDecodeError, IOError):
|
||||
pass
|
||||
if len(results) >= limit:
|
||||
break
|
||||
results.sort(key=lambda r: r.get("generated_at", ""), reverse=True)
|
||||
return JSONResponse(results[:limit])
|
||||
|
||||
|
||||
@app.get("/api/vbt/result/{filename}")
|
||||
async def get_vbt_result(filename: str):
|
||||
"""Get full VBT backtest result including equity curve."""
|
||||
for d in [BACKTEST_DIR, HISTORICAL_DIR]:
|
||||
fpath = os.path.join(d, filename)
|
||||
if os.path.exists(fpath):
|
||||
with open(fpath) as f:
|
||||
data = json.load(f)
|
||||
# Ensure equity curve is compact for transport
|
||||
ec = data.get("equity_curve", [])
|
||||
if ec and len(ec) > 500:
|
||||
step = len(ec) // 500
|
||||
data["equity_curve"] = ec[::step]
|
||||
return JSONResponse(data)
|
||||
return JSONResponse({"error": "not found"}, status_code=404)
|
||||
|
||||
|
||||
@app.get("/api/vbt/run")
|
||||
async def run_vbt_backtest(
|
||||
strategy: str = "pairs",
|
||||
interval: str = "1h",
|
||||
limit: int = 500,
|
||||
testnet: bool = False,
|
||||
):
|
||||
"""Run a new VectorBT backtest and return results."""
|
||||
try:
|
||||
from backtests.vbt_runner import VBTBacktestRunner
|
||||
runner = VBTBacktestRunner()
|
||||
from datetime import datetime
|
||||
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
result = runner.run_strategy(
|
||||
strategy=strategy, interval=interval, testnet=testnet, limit=limit
|
||||
)
|
||||
if result:
|
||||
fname = f"{strategy}_vbt_{ts}.json"
|
||||
fpath = os.path.join(BACKTEST_DIR, fname)
|
||||
with open(fpath, "w") as f:
|
||||
json.dump(result, f, default=str)
|
||||
result["filename"] = fname
|
||||
return JSONResponse(result)
|
||||
return JSONResponse({"error": "no results generated"}, status_code=500)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/api/vbt/sweep")
|
||||
async def run_vbt_sweep(strategy: str = "pairs"):
|
||||
"""Run parameter sweep and return heatmap data."""
|
||||
try:
|
||||
from backtests.vbt_runner import VBTBacktestRunner
|
||||
runner = VBTBacktestRunner()
|
||||
df = runner.param_sweep(strategy=strategy)
|
||||
if df is not None and not df.empty:
|
||||
rows = df.to_dict(orient="records")
|
||||
return JSONResponse({
|
||||
"strategy": strategy,
|
||||
"results": rows,
|
||||
"best": max(rows, key=lambda r: r.get("sharpe", -999)),
|
||||
})
|
||||
return JSONResponse({"error": "no sweep results"}, status_code=500)
|
||||
except Exception as e:
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
|
||||
@app.get("/api/vbt/strategies")
|
||||
async def list_vbt_strategies():
|
||||
"""List available strategies for VBT backtesting."""
|
||||
return JSONResponse([
|
||||
{"key": "pairs", "name": "Pairs Trading", "coins": ["BTC", "ETH"]},
|
||||
{"key": "hurst_vpin", "name": "Hurst VPIN", "coins": ["BTC"]},
|
||||
{"key": "as_mm", "name": "Avellaneda-Stoikov MM", "coins": ["BTC"]},
|
||||
{"key": "momentum", "name": "Momentum Breakout", "coins": ["BTC"]},
|
||||
{"key": "mean_rev", "name": "Mean Reversion", "coins": ["BTC"]},
|
||||
])
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# Static
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
@@ -460,6 +587,11 @@ async def root():
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
|
||||
|
||||
@app.get("/vbt")
|
||||
async def vbt_dashboard():
|
||||
return FileResponse(STATIC_DIR / "vbt.html")
|
||||
|
||||
|
||||
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FTDT Quant Lab — VectorBT Dashboard</title>
|
||||
<script src="https://cdn.plot.ly/plotly-3.1.0.min.js"></script>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{font-family:'Ubuntu',-apple-system,sans-serif;background:#0a0e17;color:#c8d6e5;min-height:100vh}
|
||||
.header{background:#111827;border-bottom:1px solid #1e293b;padding:12px 24px;display:flex;justify-content:space-between;align-items:center}
|
||||
.header h1{font-size:18px;color:#e2e8f0}
|
||||
.header span{font-size:12px;color:#64748b}
|
||||
.main{display:grid;grid-template-columns:300px 1fr;height:calc(100vh - 49px)}
|
||||
.sidebar{background:#0f172a;border-right:1px solid #1e293b;overflow-y:auto;padding:12px}
|
||||
.sidebar h3{font-size:12px;text-transform:uppercase;color:#64748b;margin:12px 0 6px;letter-spacing:1px}
|
||||
.result-item{background:#1e293b;border:1px solid #334155;border-radius:6px;padding:10px;margin-bottom:6px;cursor:pointer;transition:border-color .15s}
|
||||
.result-item:hover{border-color:#3b82f6}
|
||||
.result-item.active{border-color:#3b82f6;background:#1e3a5f}
|
||||
.result-item .name{font-size:14px;font-weight:600;color:#e2e8f0}
|
||||
.result-item .meta{font-size:11px;color:#64748b;margin-top:3px}
|
||||
.result-item .stats{display:flex;gap:10px;margin-top:5px;font-size:11px}
|
||||
.stat-pos{color:#34d399}.stat-neg{color:#f87171}.stat-neutral{color:#94a3b8}
|
||||
.content{padding:20px;overflow-y:auto}
|
||||
.metrics-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
|
||||
.metric-card{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:14px;text-align:center}
|
||||
.metric-card .label{font-size:11px;text-transform:uppercase;color:#64748b;letter-spacing:0.5px;margin-bottom:4px}
|
||||
.metric-card .value{font-size:24px;font-weight:700}
|
||||
.chart-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}
|
||||
.chart-box{background:#1e293b;border:1px solid #334155;border-radius:8px;padding:12px}
|
||||
.chart-box h4{font-size:12px;color:#64748b;text-transform:uppercase;margin-bottom:8px;letter-spacing:0.5px}
|
||||
.chart-full{grid-column:1/-1}
|
||||
.empty-state{text-align:center;padding:60px 20px;color:#64748b}
|
||||
.empty-state h2{font-size:16px;margin-bottom:8px}
|
||||
.btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:6px;font-size:13px;cursor:pointer;border:1px solid #334155;background:#1e293b;color:#c8d6e5;transition:all .15s}
|
||||
.btn:hover{background:#334155;border-color:#475569}
|
||||
.btn-primary{background:#3b82f6;border-color:#3b82f6;color:#fff}
|
||||
.btn-primary:hover{background:#2563eb}
|
||||
.btn-sm{padding:4px 10px;font-size:11px}
|
||||
.toolbar{display:flex;gap:8px;align-items:center;margin-bottom:16px;flex-wrap:wrap}
|
||||
select,input{background:#1e293b;border:1px solid #334155;color:#c8d6e5;border-radius:6px;padding:6px 10px;font-size:13px}
|
||||
select:focus,input:focus{outline:none;border-color:#3b82f6}
|
||||
.loading{text-align:center;padding:40px;color:#64748b}
|
||||
.sweep-table{width:100%;border-collapse:collapse;font-size:12px;margin-top:8px}
|
||||
.sweep-table th{text-align:left;padding:6px 10px;border-bottom:1px solid #334155;color:#64748b;font-weight:500}
|
||||
.sweep-table td{padding:5px 10px;border-bottom:1px solid #1e293b}
|
||||
.sweep-table tr:hover{background:#1e293b}
|
||||
.sweep-best{background:rgba(34,197,94,.08)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>VectorBT Dashboard <span style="font-size:11px;color:#3b82f6;margin-left:8px">Hyperliquid data</span></h1>
|
||||
<span id="last-update"></span>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="sidebar">
|
||||
<div class="toolbar" style="flex-direction:column;align-items:stretch">
|
||||
<select id="strategy-filter" onchange="loadResults()" style="width:100%">
|
||||
<option value="">All strategies</option>
|
||||
<option value="pairs">Pairs Trading</option>
|
||||
<option value="hurst_vpin">Hurst VPIN</option>
|
||||
<option value="as_mm">A-S MM</option>
|
||||
<option value="momentum">Momentum</option>
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm" onclick="runBacktest()" style="justify-content:center">+ Run Backtest</button>
|
||||
</div>
|
||||
<h3>Results</h3>
|
||||
<div id="results-list">
|
||||
<div class="loading">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content" id="content">
|
||||
<div class="empty-state">
|
||||
<h2>Select a backtest result</h2>
|
||||
<p style="font-size:13px">Choose from the sidebar or run a new VectorBT backtest</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '';
|
||||
let currentResult = null;
|
||||
|
||||
async function loadResults() {
|
||||
const strat = document.getElementById('strategy-filter').value;
|
||||
const url = strat ? `${API}/api/vbt/results?strategy=${strat}&limit=100` : `${API}/api/vbt/results?limit=100`;
|
||||
try {
|
||||
const r = await fetch(url);
|
||||
const data = await r.json();
|
||||
renderResultsList(data);
|
||||
document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
|
||||
} catch(e) {
|
||||
document.getElementById('results-list').innerHTML = '<div class="loading">Error loading</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderResultsList(results) {
|
||||
const el = document.getElementById('results-list');
|
||||
if (!results.length) {
|
||||
el.innerHTML = '<div style="padding:12px;color:#64748b;font-size:12px">No results yet</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = results.map((r,i) => `
|
||||
<div class="result-item${i===0&&!currentResult?' active':''}" onclick="selectResult('${r.filename}')" id="item-${r.filename}">
|
||||
<div class="name">${r.strategy}</div>
|
||||
<div class="meta">${r.interval || '1h'} · ${r.total_trades||0} trades · ${r.n_bars||0} bars</div>
|
||||
<div class="stats">
|
||||
<span class="${r.sharpe>0?'stat-pos':(r.sharpe<0?'stat-neg':'stat-neutral')}">Sharpe ${r.sharpe?.toFixed(2)||0}</span>
|
||||
<span class="${r.total_return_pct>0?'stat-pos':(r.total_return_pct<0?'stat-neg':'stat-neutral')}">${r.total_return_pct?.toFixed(1)||0}%</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function selectResult(filename) {
|
||||
document.querySelectorAll('.result-item').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById('item-'+filename)?.classList.add('active');
|
||||
try {
|
||||
const r = await fetch(`${API}/api/vbt/result/${filename}`);
|
||||
currentResult = await r.json();
|
||||
renderDetail(currentResult);
|
||||
} catch(e) {
|
||||
document.getElementById('content').innerHTML = '<div class="loading">Error loading result</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderDetail(r) {
|
||||
const ret = r.total_return_pct || 0;
|
||||
const dd = r.max_drawdown_pct || 0;
|
||||
const sharpe = r.sharpe || 0;
|
||||
const wr = (r.win_rate||0) * 100;
|
||||
const pf = r.profit_factor || 0;
|
||||
|
||||
let html = `
|
||||
<h3 style="margin-bottom:4px">${r.strategy} <span style="font-size:12px;color:#64748b">${r.engine||'vectorbt'} · ${r.interval||'1h'}</span></h3>
|
||||
<div style="font-size:11px;color:#64748b;margin-bottom:16px">${r.total_trades||0} trades · ${r.n_bars||0} bars · ${r.generated_at||''}</div>
|
||||
<div class="metrics-grid">
|
||||
<div class="metric-card"><div class="label">Total Return</div><div class="value ${ret>=0?'stat-pos':'stat-neg'}">${ret.toFixed(2)}%</div></div>
|
||||
<div class="metric-card"><div class="label">Sharpe</div><div class="value ${sharpe>=0?'stat-pos':'stat-neg'}">${sharpe.toFixed(2)}</div></div>
|
||||
<div class="metric-card"><div class="label">Max Drawdown</div><div class="value stat-neg">${dd.toFixed(2)}%</div></div>
|
||||
<div class="metric-card"><div class="label">Win Rate</div><div class="value ${wr>=50?'stat-pos':'stat-neg'}">${wr.toFixed(0)}%</div></div>
|
||||
<div class="metric-card"><div class="label">Profit Factor</div><div class="value ${pf>=1?'stat-pos':'stat-neg'}">${pf.toFixed(2)}</div></div>
|
||||
<div class="metric-card"><div class="label">Total Trades</div><div class="value stat-neutral">${r.total_trades||0}</div></div>
|
||||
<div class="metric-card"><div class="label">End Equity</div><div class="value stat-neutral">$${((r.end_equity||10000)).toFixed(0)}</div></div>
|
||||
<div class="metric-card"><div class="label">Sortino</div><div class="value stat-neutral">${(r.sortino||0).toFixed(2)}</div></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('content').innerHTML = html + `
|
||||
<div class="chart-row">
|
||||
<div class="chart-box chart-full"><h4>Equity Curve</h4><div id="chart-equity" style="height:300px"></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>Returns Distribution</h4><div id="chart-returns" style="height:250px"></div></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
renderCharts(r);
|
||||
}
|
||||
|
||||
function renderCharts(r) {
|
||||
const ec = r.equity_curve || [];
|
||||
if (!ec.length) return;
|
||||
|
||||
const times = ec.map(p => p.t);
|
||||
const values = ec.map(p => p.v);
|
||||
|
||||
// Equity curve
|
||||
const eqTrace = {
|
||||
x: times, y: values, type: 'scatter', mode: 'lines',
|
||||
line: {color: '#3b82f6', width: 1.5},
|
||||
fill: 'tozeroy', fillcolor: 'rgba(59,130,246,0.08)',
|
||||
name: 'Equity'
|
||||
};
|
||||
Plotly.newPlot('chart-equity', [eqTrace], {
|
||||
margin: {t:5,r:15,b:30,l:55},
|
||||
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
xaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}, showgrid: true},
|
||||
yaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}, showgrid: true},
|
||||
showlegend: false,
|
||||
}, {responsive: true, displayModeBar: false});
|
||||
|
||||
// Drawdown
|
||||
const peak = values.slice(1).reduce((arr, v, i) => {arr.push(Math.max(arr[i]||arr[0]||v, v)); return arr;}, [values[0]]);
|
||||
const dd = values.map((v, i) => i === 0 ? 0 : -((peak[i] - v) / peak[i]) * 100);
|
||||
Plotly.newPlot('chart-dd', [{
|
||||
x: times, y: dd, type: 'scatter', mode: 'none',
|
||||
fill: 'tozeroy', fillcolor: 'rgba(248,113,113,0.15)',
|
||||
line: {color: '#f87171', width: 1},
|
||||
name: 'Drawdown %'
|
||||
}], {
|
||||
margin: {t:5,r:15,b:30,l:55},
|
||||
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
xaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}},
|
||||
yaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}, ticksuffix: '%'},
|
||||
showlegend: false,
|
||||
}, {responsive: true, displayModeBar: false});
|
||||
|
||||
// Returns histogram
|
||||
if (values.length > 1) {
|
||||
const rets = values.slice(1).map((v, i) => (v - values[i]) / values[i] * 100);
|
||||
Plotly.newPlot('chart-returns', [{
|
||||
x: rets, type: 'histogram',
|
||||
marker: {color: '#3b82f6', opacity: 0.7, line: {color: '#1e293b', width: 1}},
|
||||
nbinsx: 30,
|
||||
name: 'Returns'
|
||||
}], {
|
||||
margin: {t:5,r:15,b:30,l:45},
|
||||
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
xaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}, ticksuffix: '%'},
|
||||
yaxis: {gridcolor: '#1e293b', tickfont: {color: '#64748b', size: 10}},
|
||||
showlegend: false,
|
||||
bargap: 0.05,
|
||||
}, {responsive: true, displayModeBar: false});
|
||||
}
|
||||
}
|
||||
|
||||
async function runBacktest() {
|
||||
const strat = document.getElementById('strategy-filter').value || 'pairs';
|
||||
const btn = document.querySelector('.btn-primary');
|
||||
btn.textContent = 'Running...';
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const r = await fetch(`${API}/api/vbt/run?strategy=${strat}&interval=1h&limit=500`);
|
||||
const data = await r.json();
|
||||
if (data.error) { alert('Error: ' + data.error); return; }
|
||||
loadResults();
|
||||
selectResult(data.filename);
|
||||
} catch(e) {
|
||||
alert('Failed: ' + e.message);
|
||||
} finally {
|
||||
btn.textContent = '+ Run Backtest';
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Init
|
||||
loadResults();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user