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)
';
+ 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)
';
}
document.getElementById('content').innerHTML=