feat: VBT trade log — show asset, entry/exit prices, Hyperliquid fees

Backend (vbt_runner.py):
  - Add asset (BTC/ETH) to each trade record
  - Compute per-trade fee using HL taker rate (0.05%)
    entry_fee = size * entry_px * fee_rate
    exit_fee = size * exit_px * fee_rate
  - Add pnl_gross (before fees) and pnl_net (after fees)
  - Add fee_rate field for transparency

Server (server.py):
  - Normalize old backtest trades: add missing asset, fee,
    pnl_net, pnl_gross fields
  - Holyliquid default fee rate: 0.05% taker

Frontend (vbt.html):
  - Trade log table now shows:
    Time | Side + Asset | Size | Entry | Exit | Fee | PnL (net) | Duration
  - Asset shown as inline badge in Side column
  - Fee column with explicit USD amount
  - PnL now explicitly labeled 'net' (after fees)
  - Fallback to old 'pnl' field for legacy backtest files
This commit is contained in:
ramseshk
2026-08-07 15:35:33 +08:00
parent 9ee13a45bb
commit 9d817ac2fa
3 changed files with 60 additions and 8 deletions
+29 -6
View File
@@ -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", "")),
})