fix: normalize old backtest field names in VBT dashboard API
Old files used pnl_pct (not total_return_pct), max_dd (decimal,
not max_drawdown_pct %), num_periods (not n_bars), no profit_factor.
Added _normalize_vbt_fields() that:
- Maps pnl_pct/ann_return_pct → total_return_pct
- Converts max_dd (decimal) → max_drawdown_pct (percentage)
- Maps num_periods → n_bars
- Computes profit_factor from trades (gross_wins / gross_losses)
- Computes win_rate from trades if missing
Both /api/vbt/results and /api/vbt/result/{filename} now normalise.
Verified: old Cartea-Jaimungal file now shows ret=0.82%, pf=1.18, bars=720
This commit is contained in:
+80
-13
@@ -462,6 +462,71 @@ async def get_risk_metrics():
|
|||||||
# VBT Dashboard API — VectorBT backtest results browser
|
# VBT Dashboard API — VectorBT backtest results browser
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def _normalize_vbt_fields(data: dict) -> dict:
|
||||||
|
"""Normalise old/new backtest file field names to a consistent schema."""
|
||||||
|
out = dict(data)
|
||||||
|
|
||||||
|
# total_return_pct
|
||||||
|
if "total_return_pct" not in out:
|
||||||
|
out["total_return_pct"] = out.get("pnl_pct", out.get("ann_return_pct", 0))
|
||||||
|
if out.get("total_return_pct") is None:
|
||||||
|
out["total_return_pct"] = 0
|
||||||
|
|
||||||
|
# max_drawdown_pct
|
||||||
|
if "max_drawdown_pct" not in out:
|
||||||
|
dd = out.get("max_dd_pct", out.get("max_dd"))
|
||||||
|
if dd is not None and isinstance(dd, (int, float)) and abs(dd) < 1:
|
||||||
|
dd = dd * 100 # decimal → percent
|
||||||
|
out["max_drawdown_pct"] = dd or 0
|
||||||
|
if out.get("max_drawdown_pct") is None:
|
||||||
|
out["max_drawdown_pct"] = 0
|
||||||
|
|
||||||
|
# n_bars
|
||||||
|
if "n_bars" not in out:
|
||||||
|
out["n_bars"] = out.get("num_periods", 0)
|
||||||
|
if out.get("n_bars") is None:
|
||||||
|
out["n_bars"] = 0
|
||||||
|
|
||||||
|
# profit_factor → compute from trades if missing
|
||||||
|
if "profit_factor" not in out and "trades" in out:
|
||||||
|
trades = out.get("trades", [])
|
||||||
|
if trades:
|
||||||
|
gross_win = sum(
|
||||||
|
t.get("pnl_net", t.get("pnl_gross", t.get("pnl", 0)))
|
||||||
|
for t in trades if (t.get("pnl_net", t.get("pnl_gross", t.get("pnl", 0))) or 0) > 0
|
||||||
|
)
|
||||||
|
gross_loss = abs(sum(
|
||||||
|
t.get("pnl_net", t.get("pnl_gross", t.get("pnl", 0)))
|
||||||
|
for t in trades if (t.get("pnl_net", t.get("pnl_gross", t.get("pnl", 0))) or 0) < 0
|
||||||
|
))
|
||||||
|
out["profit_factor"] = round(gross_win / gross_loss, 3) if gross_loss > 0 else 0
|
||||||
|
elif out.get("pnl_gross") is not None and out.get("fees_total") is not None:
|
||||||
|
# Synthetic: approximate PF from gross/fees relationship
|
||||||
|
pnl_gross = out.get("pnl_gross", 0)
|
||||||
|
fees = out.get("fees_total", 0)
|
||||||
|
if fees > 0:
|
||||||
|
wins = pnl_gross + fees if pnl_gross > 0 else fees
|
||||||
|
losses = fees if pnl_gross > 0 else fees - pnl_gross
|
||||||
|
out["profit_factor"] = round(wins / losses, 3) if losses > 0 else 0
|
||||||
|
if "profit_factor" not in out:
|
||||||
|
out["profit_factor"] = 0
|
||||||
|
|
||||||
|
# total_trades
|
||||||
|
if "total_trades" not in out:
|
||||||
|
out["total_trades"] = len(out.get("trades", []))
|
||||||
|
if out.get("total_trades") is None:
|
||||||
|
out["total_trades"] = 0
|
||||||
|
|
||||||
|
# win_rate → compute from trades if missing/zero
|
||||||
|
if not out.get("win_rate") and "trades" in out:
|
||||||
|
trades = out.get("trades", [])
|
||||||
|
if trades:
|
||||||
|
wins = sum(1 for t in trades if (t.get("pnl_net", t.get("pnl_gross", t.get("pnl", 0))) or 0) > 0)
|
||||||
|
out["win_rate"] = round(wins / len(trades), 3)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/vbt/results")
|
@app.get("/api/vbt/results")
|
||||||
async def list_vbt_results(strategy: str = "", limit: int = 50):
|
async def list_vbt_results(strategy: str = "", limit: int = 50):
|
||||||
"""List VectorBT backtest results with full metrics."""
|
"""List VectorBT backtest results with full metrics."""
|
||||||
@@ -478,21 +543,22 @@ async def list_vbt_results(strategy: str = "", limit: int = 50):
|
|||||||
try:
|
try:
|
||||||
with open(fpath) as f:
|
with open(fpath) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
n = _normalize_vbt_fields(data)
|
||||||
results.append({
|
results.append({
|
||||||
"filename": fname,
|
"filename": fname,
|
||||||
"strategy": data.get("strategy", "unknown"),
|
"strategy": n.get("strategy", "unknown"),
|
||||||
"engine": data.get("engine", "vectorbt"),
|
"engine": n.get("engine", "vectorbt"),
|
||||||
"interval": data.get("interval", "1h"),
|
"interval": n.get("interval", "1h"),
|
||||||
"sharpe": data.get("sharpe", 0),
|
"sharpe": n.get("sharpe", 0),
|
||||||
"sortino": data.get("sortino", 0),
|
"sortino": n.get("sortino", 0),
|
||||||
"total_return_pct": data.get("total_return_pct", 0),
|
"total_return_pct": n["total_return_pct"],
|
||||||
"max_drawdown_pct": data.get("max_drawdown_pct", 0),
|
"max_drawdown_pct": n["max_drawdown_pct"],
|
||||||
"win_rate": data.get("win_rate", 0),
|
"win_rate": n.get("win_rate", 0),
|
||||||
"profit_factor": data.get("profit_factor", 0),
|
"profit_factor": n["profit_factor"],
|
||||||
"total_trades": data.get("total_trades", 0),
|
"total_trades": n["total_trades"],
|
||||||
"n_bars": data.get("n_bars", 0),
|
"n_bars": n["n_bars"],
|
||||||
"generated_at": data.get("generated_at", ""),
|
"generated_at": n.get("generated_at", ""),
|
||||||
"has_equity_curve": bool(data.get("equity_curve")),
|
"has_equity_curve": bool(n.get("equity_curve")),
|
||||||
})
|
})
|
||||||
except (json.JSONDecodeError, IOError):
|
except (json.JSONDecodeError, IOError):
|
||||||
pass
|
pass
|
||||||
@@ -510,6 +576,7 @@ async def get_vbt_result(filename: str):
|
|||||||
if os.path.exists(fpath):
|
if os.path.exists(fpath):
|
||||||
with open(fpath) as f:
|
with open(fpath) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
data = _normalize_vbt_fields(data)
|
||||||
# Ensure equity curve is compact for transport
|
# Ensure equity curve is compact for transport
|
||||||
ec = data.get("equity_curve", [])
|
ec = data.get("equity_curve", [])
|
||||||
if ec and len(ec) > 500:
|
if ec and len(ec) > 500:
|
||||||
|
|||||||
Reference in New Issue
Block a user