diff --git a/backtests/vbt_runner.py b/backtests/vbt_runner.py index 9a6a9b3..74fa0e3 100644 --- a/backtests/vbt_runner.py +++ b/backtests/vbt_runner.py @@ -442,18 +442,41 @@ class VBTBacktestRunner: return coin_map.get(strategy, ["BTC"]) def _extract_metrics(self, pf, stats, strategy, interval, n_bars) -> dict: - # Extract trade records from VectorBT portfolio + # Determine which coin this strategy trades + main_coin = self._get_coins(strategy)[0] + asset = main_coin if main_coin else "BTC" + trades = [] try: trade_records = pf.trades.records_readable for _, t in trade_records.iterrows(): + side = "BUY" if str(t.get("Direction", "")) == "Long" else "SELL" + entry_px = round(float(t.get("Avg Entry Price", 0)), 2) + exit_px = round(float(t.get("Avg Exit Price", 0)), 2) + size = round(float(t.get("Size", 0)), 6) + notional = size * entry_px + + # VectorBT's PnL already accounts for fees when fees= is set on Portfolio + pnl_vbt = round(float(t.get("PnL", 0)), 4) + fee_rate = self._fee_rate # applied per side by VBT + entry_fee = round(notional * fee_rate, 6) + exit_fee = round(size * exit_px * fee_rate, 6) + total_fee = round(entry_fee + exit_fee, 6) + + # Gross PnL = net + fees + gross_pnl = round(pnl_vbt + total_fee, 4) + trades.append({ "time": str(t.get("Exit Timestamp", t.get("Entry Timestamp", "")))[:19], - "side": "BUY" if str(t.get("Direction", "")) == "Long" else "SELL", - "size": round(float(t.get("Size", 0)), 6), - "entry_px": round(float(t.get("Avg Entry Price", 0)), 2), - "exit_px": round(float(t.get("Avg Exit Price", 0)), 2), - "pnl": round(float(t.get("PnL", 0)), 4), + "side": side, + "asset": asset, + "size": size, + "entry_px": entry_px, + "exit_px": exit_px, + "pnl_gross": gross_pnl, + "pnl_net": pnl_vbt, + "fee": total_fee, + "fee_rate": f"{fee_rate*100:.3f}%", "return_pct": round(float(t.get("Return", 0)) * 100, 3), "duration": str(t.get("Duration", "")), }) diff --git a/dashboard/server.py b/dashboard/server.py index 8534f30..6d52fd5 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -576,6 +576,29 @@ def _normalize_vbt_fields(data: dict) -> dict: 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) + # Normalize trade records: add asset, fee, pnl_net/pnl_gross for old files + trades = out.get("trades", []) + if trades: + asset = _infer_asset(out.get("strategy", ""), "") + hl_fee = 0.0005 # default Hyperliquid taker rate + for t in trades: + if not t.get("asset"): + t["asset"] = asset + if not t.get("fee"): + entry_px = float(t.get("entry_px", 0)) + exit_px = float(t.get("exit_px", 0)) + size = float(t.get("size", 0)) + notional_entry = size * entry_px + notional_exit = size * exit_px + t["fee"] = round((notional_entry + notional_exit) * hl_fee, 6) + t["fee_rate"] = f"{hl_fee*100:.3f}%" + if not t.get("pnl_net"): + t["pnl_net"] = t.get("pnl", 0) + if not t.get("pnl_gross"): + pnl_net = float(t.get("pnl_net", t.get("pnl", 0))) + fee = float(t.get("fee", 0)) + t["pnl_gross"] = round(pnl_net + fee, 4) + return out diff --git a/dashboard/static/vbt.html b/dashboard/static/vbt.html index b4b33f6..f2af7cc 100644 --- a/dashboard/static/vbt.html +++ b/dashboard/static/vbt.html @@ -252,8 +252,14 @@ function renderDetail(r){ let trades_html=''; if(trades.length){ - const rows=trades.map(t=>''+String(t.time||'').substring(0,19)+''+String(t.side||'')+''+Number(t.size||0).toFixed(6)+'$'+Number(t.entry_px||0).toFixed(1)+'$'+Number(t.exit_px||0).toFixed(1)+'$'+Number(t.pnl||0).toFixed(4)+''+String(t.duration||'')+'').join(''); - trades_html='

Trade Log ('+trades.length+' trades)

'+rows+'
TimeSideSizeEntryExitPnLDuration
'; + const rows=trades.map(t=>{ + const pnl_net=Number(t.pnl_net??t.pnl??0); + const fee=Number(t.fee??0); + const pnlCls=pnl_net>=0?'pnl-pos':'pnl-neg'; + const assetBadge = t.asset ? ''+t.asset+'' : ''; + return ''+String(t.time||'').substring(0,19)+''+String(t.side||'')+assetBadge+''+Number(t.size||0).toFixed(6)+'$'+Number(t.entry_px||0).toFixed(1)+'$'+Number(t.exit_px||0).toFixed(1)+'$'+fee.toFixed(6)+'$'+pnl_net.toFixed(4)+''+String(t.duration||'')+''; + }).join(''); + trades_html='

Trade Log ('+trades.length+' trades)

'+rows+'
TimeSideSizeEntryExitFeePnL (net)Duration
'; } document.getElementById('content').innerHTML=