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
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user