Add real historical backtesting with Hyperliquid mainnet candle data
backtests/historical_runner.py: Fetches real 1h candles from Hyperliquid
mainnet API (candleSnapshot endpoint). Runs all 7 strategies against
actual BTC price history (721 candles, 30 days, $63,024→$63,605).
Each strategy's signal logic operates on real OHLCV data with
configurable fee tiers. Saves to backtests/results/historical/.
Results on 30d BTC data at VIP0:
Mean Reversion: +93.87% net (Sharpe 0.94)
Order Book Imbalance: +54.31% net (Sharpe 1.03)
Avellaneda-Stoikov: -1.02% net (Sharpe -0.13)
Iceberg Detection: -33.20% net
Momentum Breakout: -54.72% net
Server: Added /api/backtests/historical (list) and
/api/backtest/historical/{name} (full data) endpoints.
Dashboard: Added "Historical" tab with "Real Data" badge. Cards show
coin + mainnet source. Click opens the same detail panel with fee
tier dropdown and equity chart.
This commit is contained in:
@@ -28,6 +28,7 @@ from fastapi.responses import FileResponse, JSONResponse
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from config.fee_tiers import get_perp_fees, PERPS_TIERS, STAKING_TIERS, STRATEGY_FEE_MODELS
|
||||
from common.risk import risk_summary
|
||||
import uvicorn
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
@@ -37,6 +38,7 @@ 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
|
||||
@@ -257,6 +259,47 @@ async def recalc_backtest(name: str, fee_tier: int = 0, staking_tier: str = "non
|
||||
})
|
||||
|
||||
|
||||
@app.get("/api/backtests/historical")
|
||||
async def list_historical_backtests():
|
||||
"""List historical (real data) backtest results."""
|
||||
results = []
|
||||
d = HISTORICAL_DIR
|
||||
if os.path.isdir(d):
|
||||
for fname in sorted(os.listdir(d), reverse=True):
|
||||
if fname.endswith(".json"):
|
||||
fpath = os.path.join(d, fname)
|
||||
try:
|
||||
with open(fpath) as f:
|
||||
data = json.load(f)
|
||||
results.append({
|
||||
"name": fname.replace(".json", ""),
|
||||
"strategy": data.get("strategy", "unknown"),
|
||||
"coin": data.get("coin", "?"),
|
||||
"start": data.get("start_time"),
|
||||
"end": data.get("end_time"),
|
||||
"sharpe": data.get("sharpe", 0),
|
||||
"sortino": data.get("sortino", 0),
|
||||
"pnl_pct": data.get("pnl_pct", 0),
|
||||
"max_dd": data.get("max_dd", 0),
|
||||
"win_rate": data.get("win_rate", 0),
|
||||
"total_trades": data.get("total_trades", 0),
|
||||
"data_source": "Hyperliquid Mainnet",
|
||||
})
|
||||
except (json.JSONDecodeError, IOError):
|
||||
pass
|
||||
return JSONResponse(results)
|
||||
|
||||
|
||||
@app.get("/api/backtest/historical/{name}")
|
||||
async def get_historical_backtest(name: str):
|
||||
"""Get full historical backtest result."""
|
||||
fpath = os.path.join(HISTORICAL_DIR, f"{name}.json")
|
||||
if os.path.exists(fpath):
|
||||
with open(fpath) as f:
|
||||
return JSONResponse(json.load(f))
|
||||
return JSONResponse({"error": "not found"}, status_code=404)
|
||||
|
||||
|
||||
@app.get("/api/backtest/{name}/csv")
|
||||
async def get_backtest_csv(name: str):
|
||||
"""Download backtest trades as CSV."""
|
||||
@@ -280,6 +323,48 @@ async def get_backtest_csv(name: str):
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/risk")
|
||||
async def get_risk_metrics():
|
||||
"""Compute risk analytics from the latest paper metrics."""
|
||||
paper = read_paper_metrics()
|
||||
equity_history = paper.get("equity_history", [])
|
||||
strategy_equity = paper.get("strategy_equity", {})
|
||||
|
||||
if not equity_history:
|
||||
return JSONResponse({"error": "no equity history available"}, status_code=404)
|
||||
|
||||
summary = risk_summary(equity_history, strategy_equity)
|
||||
|
||||
# Build a compact correlation text summary for the frontend
|
||||
corr = summary.get("correlation", {})
|
||||
corr_summary = []
|
||||
names = sorted(corr.keys())
|
||||
for i, n1 in enumerate(names):
|
||||
for n2 in names[i + 1:]:
|
||||
val = corr.get(n1, {}).get(n2, 0)
|
||||
if abs(val) > 0.3: # only show meaningful correlations
|
||||
corr_summary.append({
|
||||
"pair": f"{n1} ↔ {n2}",
|
||||
"correlation": round(val, 3),
|
||||
"level": "high" if abs(val) > 0.7 else "medium",
|
||||
})
|
||||
corr_summary.sort(key=lambda x: -abs(x["correlation"]))
|
||||
|
||||
return JSONResponse({
|
||||
"portfolio": {
|
||||
"var_95": summary["var_95"],
|
||||
"cvar_95": summary["cvar_95"],
|
||||
"max_drawdown": summary["max_drawdown"],
|
||||
"calmar_ratio": summary["calmar_ratio"],
|
||||
"sharpe": summary["sharpe"],
|
||||
"sortino": summary["sortino"],
|
||||
"num_observations": summary["num_observations"],
|
||||
},
|
||||
"per_strategy": summary.get("per_strategy", {}),
|
||||
"correlation_summary": corr_summary,
|
||||
"correlation_matrix": corr,
|
||||
})
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# Static
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
@@ -76,6 +76,19 @@ body{background:var(--bg);color:var(--hi);font-family:var(--f);min-height:100vh;
|
||||
footer{text-align:center;padding:30px;font-size:10px;color:#2a2a35}
|
||||
footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)}
|
||||
|
||||
/* Risk Analytics panel — collapsible */
|
||||
.risk-wrap{max-width:1440px;margin:0 auto 20px;padding:0 24px}
|
||||
.risk-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;background:none;border:1px solid var(--ln);border-radius:var(--ra);color:var(--tx);font-family:var(--f);font-size:11px;font-weight:600;padding:10px 16px;text-transform:uppercase;letter-spacing:.5px;transition:all .15s}
|
||||
.risk-toggle:hover{color:var(--hi);border-color:var(--hr)}
|
||||
.risk-toggle .arrow{display:inline-block;transition:transform .2s;font-size:10px}
|
||||
.risk-toggle.open .arrow{transform:rotate(90deg)}
|
||||
.risk-panel{display:none;background:var(--srf);border:1px solid var(--ln);border-radius:var(--ra);padding:16px;margin-top:8px}
|
||||
.risk-panel.show{display:block}
|
||||
.risk-corr{font-family:var(--m);font-size:10px;color:var(--tx);line-height:1.8;margin-top:12px;padding:10px;background:rgba(0,0,0,.2);border-radius:6px;max-height:200px;overflow-y:auto}
|
||||
.risk-corr .corr-high{color:var(--rd)}
|
||||
.risk-corr .corr-med{color:var(--am)}
|
||||
.risk-corr .corr-low{color:var(--tx)}
|
||||
|
||||
@media(max-width:768px){
|
||||
.topbar{padding:10px 14px;flex-direction:column;align-items:flex-start}
|
||||
.tabs{padding:0 14px;top:88px;overflow-x:auto;white-space:nowrap}
|
||||
@@ -104,6 +117,7 @@ footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)}
|
||||
<button class="tab on" id="tl-live" onclick="switchTab('live')">Live<span class="badge test">Testnet</span></button>
|
||||
<button class="tab" id="tl-paper" onclick="switchTab('paper')">Paper<span class="badge main">$100K Mainnet</span></button>
|
||||
<button class="tab" id="tl-backtest" onclick="switchTab('backtest')">Backtest</button>
|
||||
<button class="tab" id="tl-historical" onclick="switchTab('historical')">Historical<span class="badge main">Real Data</span></button>
|
||||
</div>
|
||||
<!-- Main -->
|
||||
<div class="main-wrap">
|
||||
@@ -118,7 +132,19 @@ footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)}
|
||||
<div class="panel" id="pnl-backtest">
|
||||
<div class="sgrid" id="bt-sgrid"></div>
|
||||
</div>
|
||||
<div class="panel" id="pnl-historical">
|
||||
<div class="sgrid" id="hist-sgrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Risk Analytics -->
|
||||
<div class="risk-wrap">
|
||||
<button class="risk-toggle" onclick="toggleRisk()" id="risk-btn"><span class="arrow">▶</span> Risk Analytics</button>
|
||||
<div class="risk-panel" id="risk-panel">
|
||||
<div class="stats-row" id="risk-stats" style="margin-bottom:12px"></div>
|
||||
<div class="risk-corr" id="risk-corr"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer><a href="https://git.ftdt.io/rams/ftdt-quant-lab" target="_blank">rams/ftdt-quant-lab</a> · 12 strategies · $100K paper · Hyperliquid</footer>
|
||||
|
||||
<!-- Detail Overlay -->
|
||||
@@ -193,6 +219,7 @@ function switchTab(t){
|
||||
if(t==='live'&&lastData)renLive(lastData);
|
||||
if(t==='paper'&&lastPaper)renPaper(lastPaper);
|
||||
if(t==='backtest')loadBT();
|
||||
if(t==='historical')loadHistBT();
|
||||
}
|
||||
|
||||
// ═══════════ Render strategy cards ═══════════
|
||||
@@ -375,6 +402,43 @@ function loadBT(){
|
||||
|
||||
// ═══════════ Init ═══════════
|
||||
initDetChart();connect();loadBT();
|
||||
// ═══════════ Risk Analytics ═══════════
|
||||
function toggleRisk(){
|
||||
var p=document.getElementById('risk-panel'),b=document.getElementById('risk-btn');
|
||||
p.classList.toggle('show');b.classList.toggle('open');
|
||||
if(p.classList.contains('show')&&!p.dataset.loaded){loadRisk();p.dataset.loaded='1'}
|
||||
}
|
||||
function loadRisk(){
|
||||
fetch('/cv/api/risk').then(function(r){return r.json()}).then(function(d){
|
||||
if(d.error){document.getElementById('risk-stats').innerHTML='<div style="color:var(--tx);padding:8px">'+d.error+'</div>';return}
|
||||
var pf=d.portfolio||{};
|
||||
document.getElementById('risk-stats').innerHTML=
|
||||
'<div class="stat"><div class="lbl">VaR 95%</div><div class="val dn">'+(pf.var_95*100).toFixed(2)+'%</div></div>'+
|
||||
'<div class="stat"><div class="lbl">CVaR 95%</div><div class="val dn">'+(pf.cvar_95*100).toFixed(2)+'%</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Max DD</div><div class="val dn">'+(pf.max_drawdown*100).toFixed(2)+'%</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Calmar</div><div class="val '+(pf.calmar_ratio>=0?'up':'dn')+'">'+pf.calmar_ratio.toFixed(2)+'</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Sharpe</div><div class="val '+(pf.sharpe>=0?'up':'dn')+'">'+pf.sharpe.toFixed(2)+'</div></div>'+
|
||||
'<div class="stat"><div class="lbl">Sortino</div><div class="val">'+pf.sortino.toFixed(2)+'</div></div>';
|
||||
// Correlation summary
|
||||
var cs=d.correlation_summary||[];
|
||||
var ch='<div style="font-size:10px;color:var(--tx);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">Strategy Correlations (|ρ| > 0.3)</div>';
|
||||
if(cs.length===0){ch+='<span style="color:var(--tx)">No significant correlations found — strategies are well-diversified.</span>'}
|
||||
else{for(var i=0;i<cs.length;i++){var c=cs[i],cls=c.level==='high'?'corr-high':'corr-med';ch+='<div><span class="'+cls+'">ρ='+(c.correlation>=0?'+':'')+c.correlation.toFixed(3)+'</span> '+c.pair+'</div>'}}
|
||||
document.getElementById('risk-corr').innerHTML=ch;
|
||||
// Mark loaded + store timestamp
|
||||
window._riskLoaded=Date.now();
|
||||
}).catch(function(e){document.getElementById('risk-stats').innerHTML='<div style="color:var(--rd);padding:8px">Failed: '+e.message+'</div>'})
|
||||
}
|
||||
// Auto-refresh risk panel when paper data updates (throttled to every 30s)
|
||||
var _origRenPaper=renPaper;
|
||||
renPaper=function(d){
|
||||
_origRenPaper(d);
|
||||
var p=document.getElementById('risk-panel');
|
||||
if(p&&p.classList.contains('show')&&(!window._riskLoaded||Date.now()-window._riskLoaded>30000)){
|
||||
loadRisk();
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user