feat: Hyperliquid fee schedule — all tiers, staking, maker rebates

config/fee_tiers.py — complete rewrite:
  - 7 perps fee tiers (T0-T6) matching HL docs:
    T0: 0.045/0.015% → T6: 0.024/0.000%
  - 7 spot fee tiers (T0-T6):
    T0: 0.070/0.040% → T6: 0.025/0.000%
  - 7 staking tiers (none → diamond):
    multiplier 1.00 → 0.60 (40% discount)
  - 3 maker rebate tiers (>0.5%, >1.5%, >3% maker ratio)
    extra -0.001% to -0.003% on positive maker rates
  - compute_trade_fees() — per-trade fee breakdown
  - fee_tier_from_volume(), staking_tier_from_hype()
  - STRATEGY_FEE_MODELS: maker/taker classification per strategy

backtests/vbt_runner.py:
  - Accept vip_tier, staking_tier, maker_rebate_tier at init
  - Auto-detect fee model per strategy (maker vs taker)
  - compute_trade_fees() for per-trade fee calculation
  - Include fee_info in result JSON

dashboard/server.py:
  - /api/vbt/run accepts fee_tier/aking_tier/maker_rebate params
  - Trade normalization uses proper HL fee schedule per strategy
  - /api/vbt/result/{filename}/recalc — recalc trades with new tiers
  - /api/vbt/fee_tiers — get full fee schedule as JSON

dashboard/static/vbt.html:
  - Fee tier selector (T0-T6) + staking tier selector
  - Auto-recalculate on tier change when a result is selected
  - Fee rate shown in trade log header (e.g. 0.045%)
This commit is contained in:
ramseshk
2026-08-07 15:44:43 +08:00
parent 9d817ac2fa
commit 0162e83138
4 changed files with 311 additions and 65 deletions
+41 -17
View File
@@ -300,9 +300,15 @@ def _hurst_rs_series(returns_series: pd.Series) -> float:
class VBTBacktestRunner:
"""VectorBT-powered backtesting on Hyperliquid candle data."""
def __init__(self, fee_rate: float = 0.0005):
def __init__(self, fee_rate: float | None = None,
vip_tier: int = 0, staking_tier: str = "none", maker_rebate_tier: int = 0):
from config.fee_tiers import get_perp_fees, get_strategy_fee_model
self._provider = HyperliquidDataProvider()
self._fee_rate = fee_rate
self._vip_tier = vip_tier
self._staking_tier = staking_tier
self._maker_rebate_tier = maker_rebate_tier
self._fee_rate = fee_rate if fee_rate is not None else get_perp_fees(vip_tier, staking_tier, "taker", maker_rebate_tier)
self._maker_rate = get_perp_fees(vip_tier, staking_tier, "maker", maker_rebate_tier)
def run_strategy(
self,
@@ -344,11 +350,14 @@ class VBTBacktestRunner:
return self._empty_result(strategy, interval)
try:
from config.fee_tiers import get_strategy_fee_model
fee_model = get_strategy_fee_model(strategy)
effective_fee = self._maker_rate if fee_model == "maker" else self._fee_rate
pf = vbt.Portfolio.from_signals(
close=close,
entries=entries,
exits=exits,
fees=self._fee_rate,
fees=effective_fee,
slippage=0.001,
freq=INTERVAL_MAP.get(interval, "1h"),
init_cash=10000.0,
@@ -442,9 +451,19 @@ class VBTBacktestRunner:
return coin_map.get(strategy, ["BTC"])
def _extract_metrics(self, pf, stats, strategy, interval, n_bars) -> dict:
# Determine which coin this strategy trades
from config.fee_tiers import compute_trade_fees, get_strategy_fee_model
main_coin = self._get_coins(strategy)[0]
asset = main_coin if main_coin else "BTC"
fee_model = get_strategy_fee_model(strategy)
vip = self._vip_tier
staking = self._staking_tier
rebate = self._maker_rebate_tier
# Summary fee info from compute_trade_fees at nominal size
fee_info = compute_trade_fees("BUY", 0.001, 100000.0, 100000.0,
vip_tier=vip, staking_tier=staking,
fee_model=fee_model, maker_rebate_tier=rebate)
trades = []
try:
@@ -454,17 +473,21 @@ class VBTBacktestRunner:
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)
# Compute actual per-trade fees using HL schedule
ft = compute_trade_fees(
side=side,
size=size,
entry_px=entry_px,
exit_px=exit_px,
vip_tier=vip,
staking_tier=staking,
fee_model=fee_model,
maker_rebate_tier=rebate,
)
# Gross PnL = net + fees
gross_pnl = round(pnl_vbt + total_fee, 4)
pnl_gross_raw = float(t.get("PnL", 0))
pnl_net = round(pnl_gross_raw - ft["total_fee"], 4)
trades.append({
"time": str(t.get("Exit Timestamp", t.get("Entry Timestamp", "")))[:19],
@@ -473,10 +496,10 @@ class VBTBacktestRunner:
"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}%",
"pnl_gross": round(pnl_gross_raw, 4),
"pnl_net": pnl_net,
"fee": ft["total_fee"],
"fee_rate_pct": fee_info["effective_rate_pct"],
"return_pct": round(float(t.get("Return", 0)) * 100, 3),
"duration": str(t.get("Duration", "")),
})
@@ -499,6 +522,7 @@ class VBTBacktestRunner:
"expectancy": round(float(stats.get("Expectancy", 0)), 3),
"trades": trades,
"params": _strategy_params(strategy),
"fee_info": fee_info,
}
def _empty_result(self, strategy: str, interval: str) -> dict: