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:
+41
-17
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user