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:
+152
-36
@@ -1,64 +1,104 @@
|
||||
"""
|
||||
Hyperliquid fee tiers — perps and spot, base rates + staking discounts.
|
||||
Hyperliquid fee schedule — perps, spot, staking discounts, maker rebates.
|
||||
|
||||
Source: https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees
|
||||
|
||||
Fee = base_rate × staking_multiplier
|
||||
Staking tiers are based on staked HYPE tokens.
|
||||
Perps fee tiers: 14d rolling volume determines base taker/maker rate.
|
||||
Staking tiers: staked HYPE discount applied on top of base rates.
|
||||
Maker rebates: extra rebate for high maker-volume-ratio accounts.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
from __future__ import annotations
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Perps fee tiers — base taker & maker rates (decimal)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
# ── Perps fee tiers (base rates) ──
|
||||
PERPS_TIERS = {
|
||||
0: {"name": "Tier 0", "volume": 0, "taker": 0.00045, "maker": 0.00015},
|
||||
1: {"name": "Tier 1", "volume": 5_000_000, "taker": 0.00040, "maker": 0.00012},
|
||||
2: {"name": "Tier 2", "volume": 25_000_000, "taker": 0.00035, "maker": 0.00008},
|
||||
3: {"name": "Tier 3", "volume": 100_000_000,"taker": 0.00030, "maker": 0.00004},
|
||||
4: {"name": "Tier 4", "volume": 500_000_000, "taker": 0.00028, "maker": 0.00000},
|
||||
5: {"name": "Tier 5", "volume": 2_000_000_000, "taker": 0.00026, "maker": 0.00000},
|
||||
6: {"name": "Tier 6", "volume": 7_000_000_000, "taker": 0.00024, "maker": 0.00000},
|
||||
0: {"name": "Tier 0 (<$5M)", "min_volume": 0, "taker": 0.00045, "maker": 0.00015},
|
||||
1: {"name": "Tier 1 (>$5M)", "min_volume": 5_000_000, "taker": 0.00040, "maker": 0.00012},
|
||||
2: {"name": "Tier 2 (>$25M)", "min_volume": 25_000_000, "taker": 0.00035, "maker": 0.00008},
|
||||
3: {"name": "Tier 3 (>$100M)", "min_volume": 100_000_000, "taker": 0.00030, "maker": 0.00004},
|
||||
4: {"name": "Tier 4 (>$500M)", "min_volume": 500_000_000, "taker": 0.00028, "maker": 0.00000},
|
||||
5: {"name": "Tier 5 (>$2B)", "min_volume": 2_000_000_000,"taker": 0.00026, "maker": 0.00000},
|
||||
6: {"name": "Tier 6 (>$7B)", "min_volume": 7_000_000_000,"taker": 0.00024, "maker": 0.00000},
|
||||
}
|
||||
|
||||
# ── Spot fee tiers (base rates) ──
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Spot fee tiers — base taker & maker rates (decimal)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
SPOT_TIERS = {
|
||||
0: {"name": "Tier 0", "volume": 0, "taker": 0.00070, "maker": 0.00040},
|
||||
1: {"name": "Tier 1", "volume": 100_000, "taker": 0.00060, "maker": 0.00030},
|
||||
2: {"name": "Tier 2", "volume": 1_000_000, "taker": 0.00050, "maker": 0.00020},
|
||||
3: {"name": "Tier 3", "volume": 10_000_000, "taker": 0.00040, "maker": 0.00010},
|
||||
4: {"name": "Tier 4", "volume": 50_000_000, "taker": 0.00030, "maker": 0.00005},
|
||||
5: {"name": "Tier 5", "volume": 200_000_000, "taker": 0.00020, "maker": 0.00000},
|
||||
6: {"name": "Tier 6", "volume": 1_000_000_000,"taker":0.00010,"maker": -0.00005},
|
||||
0: {"name": "Tier 0 (<$5M)", "min_volume": 0, "taker": 0.00070, "maker": 0.00040},
|
||||
1: {"name": "Tier 1 (>$5M)", "min_volume": 5_000_000, "taker": 0.00060, "maker": 0.00030},
|
||||
2: {"name": "Tier 2 (>$25M)", "min_volume": 25_000_000, "taker": 0.00050, "maker": 0.00020},
|
||||
3: {"name": "Tier 3 (>$100M)", "min_volume": 100_000_000, "taker": 0.00040, "maker": 0.00010},
|
||||
4: {"name": "Tier 4 (>$500M)", "min_volume": 500_000_000, "taker": 0.00035, "maker": 0.00000},
|
||||
5: {"name": "Tier 5 (>$2B)", "min_volume": 2_000_000_000,"taker": 0.00030, "maker": 0.00000},
|
||||
6: {"name": "Tier 6 (>$7B)", "min_volume": 7_000_000_000,"taker": 0.00025, "maker": 0.00000},
|
||||
}
|
||||
|
||||
# ── Staking discount multipliers ──
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Staking tiers — discount multiplier on all fees
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
STAKING_TIERS = {
|
||||
"none": {"name": "No Stake", "multiplier": 1.00},
|
||||
"wood": {"name": "Wood", "multiplier": 0.95},
|
||||
"bronze": {"name": "Bronze", "multiplier": 0.90},
|
||||
"silver": {"name": "Silver", "multiplier": 0.85},
|
||||
"gold": {"name": "Gold", "multiplier": 0.80},
|
||||
"platinum": {"name": "Platinum", "multiplier": 0.70},
|
||||
"diamond": {"name": "Diamond", "multiplier": 0.60},
|
||||
"none": {"name": "No Stake", "min_hype": 0, "multiplier": 1.00},
|
||||
"wood": {"name": "Wood (>10)", "min_hype": 10, "multiplier": 0.95},
|
||||
"bronze": {"name": "Bronze (>100)", "min_hype": 100, "multiplier": 0.90},
|
||||
"silver": {"name": "Silver (>1K)", "min_hype": 1_000, "multiplier": 0.85},
|
||||
"gold": {"name": "Gold (>10K)", "min_hype": 10_000, "multiplier": 0.80},
|
||||
"platinum": {"name": "Platinum (>100K)", "min_hype": 100_000, "multiplier": 0.70},
|
||||
"diamond": {"name": "Diamond (>500K)","min_hype": 500_000, "multiplier": 0.60},
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Maker rebates — extra rebate for high maker-volume accounts
|
||||
# ONLY applies when maker fee > 0 (doesn't stack with zero maker)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
MAKER_REBATES = {
|
||||
0: {"name": "No rebate", "min_maker_ratio": 0.0, "rebate": 0},
|
||||
1: {"name": "Rebate 1 (>0.5%)", "min_maker_ratio": 0.005, "rebate": -0.00001},
|
||||
2: {"name": "Rebate 2 (>1.5%)", "min_maker_ratio": 0.015, "rebate": -0.00002},
|
||||
3: {"name": "Rebate 3 (>3%)", "min_maker_ratio": 0.03, "rebate": -0.00003},
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Compute effective rates
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def effective_rate(base_rate: float, staking_tier: str = "none") -> float:
|
||||
"""Calculate effective fee rate after staking discount."""
|
||||
"""Apply staking discount to base rate."""
|
||||
mult = STAKING_TIERS.get(staking_tier, STAKING_TIERS["none"])["multiplier"]
|
||||
return base_rate * mult
|
||||
|
||||
|
||||
def get_perp_fees(vip_tier: int, staking_tier: str = "none", fee_model: str = "taker") -> float:
|
||||
"""Get effective perp fee for a given VIP tier and staking tier."""
|
||||
def get_perp_fees(vip_tier: int, staking_tier: str = "none", fee_model: str = "taker",
|
||||
maker_rebate_tier: int = 0) -> float:
|
||||
"""Get effective perp fee rate.
|
||||
|
||||
Args:
|
||||
vip_tier: 0-6 volume tier
|
||||
staking_tier: none/wood/bronze/silver/gold/platinum/diamond
|
||||
fee_model: 'taker' or 'maker'
|
||||
maker_rebate_tier: 0-3 maker volume rebate tier
|
||||
|
||||
Returns fee rate as decimal (e.g. 0.00045 = 0.045%)
|
||||
"""
|
||||
tier = PERPS_TIERS.get(vip_tier, PERPS_TIERS[0])
|
||||
base = tier[fee_model] if fee_model in ("taker", "maker") else tier["taker"]
|
||||
return effective_rate(base, staking_tier)
|
||||
rate = effective_rate(base, staking_tier)
|
||||
|
||||
if fee_model == "maker" and base > 0 and maker_rebate_tier > 0:
|
||||
rebate = MAKER_REBATES.get(maker_rebate_tier, {}).get("rebate", 0)
|
||||
rate = rate + rebate
|
||||
|
||||
return rate
|
||||
|
||||
|
||||
def get_spot_fees(vip_tier: int, staking_tier: str = "none", fee_model: str = "taker") -> float:
|
||||
"""Get effective spot fee for a given VIP tier and staking tier."""
|
||||
"""Get effective spot fee rate."""
|
||||
tier = SPOT_TIERS.get(vip_tier, SPOT_TIERS[0])
|
||||
base = tier[fee_model] if fee_model in ("taker", "maker") else tier["taker"]
|
||||
return effective_rate(base, staking_tier)
|
||||
@@ -69,12 +109,69 @@ def fee_tier_from_volume(volume_14d: float, market: str = "perps") -> int:
|
||||
tiers = PERPS_TIERS if market == "perps" else SPOT_TIERS
|
||||
current = 0
|
||||
for t in sorted(tiers.keys()):
|
||||
if volume_14d >= tiers[t]["volume"]:
|
||||
if volume_14d >= tiers[t]["min_volume"]:
|
||||
current = t
|
||||
return current
|
||||
|
||||
|
||||
# ── Strategy-specific defaults (matching existing classification) ──
|
||||
def staking_tier_from_hype(hype_staked: float) -> str:
|
||||
"""Determine staking tier from staked HYPE amount."""
|
||||
current = "none"
|
||||
for name, info in STAKING_TIERS.items():
|
||||
if hype_staked >= info["min_hype"]:
|
||||
current = name
|
||||
return current
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Trade PnL computation
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
def compute_trade_fees(
|
||||
side: str,
|
||||
size: float,
|
||||
entry_px: float,
|
||||
exit_px: float,
|
||||
vip_tier: int = 0,
|
||||
staking_tier: str = "none",
|
||||
fee_model: str = "taker",
|
||||
maker_rebate_tier: int = 0,
|
||||
) -> dict:
|
||||
"""Compute entry, exit, and total fees for a round-trip trade.
|
||||
|
||||
Returns dict with entry_fee, exit_fee, total_fee, effective_rate_taker, effective_rate_maker.
|
||||
"""
|
||||
notional_entry = size * entry_px
|
||||
notional_exit = size * exit_px
|
||||
|
||||
taker_rate = get_perp_fees(vip_tier, staking_tier, "taker", maker_rebate_tier)
|
||||
maker_rate = get_perp_fees(vip_tier, staking_tier, "maker", maker_rebate_tier)
|
||||
|
||||
rate = taker_rate if fee_model == "taker" else maker_rate
|
||||
|
||||
entry_fee = notional_entry * rate
|
||||
exit_fee = notional_exit * rate
|
||||
total_fee = entry_fee + exit_fee
|
||||
|
||||
return {
|
||||
"entry_fee": round(entry_fee, 6),
|
||||
"exit_fee": round(exit_fee, 6),
|
||||
"total_fee": round(total_fee, 6),
|
||||
"taker_rate_pct": round(taker_rate * 100, 4),
|
||||
"maker_rate_pct": round(maker_rate * 100, 4),
|
||||
"effective_rate_pct": round(rate * 100, 4) if rate else 0,
|
||||
"vip_tier": vip_tier,
|
||||
"staking_tier": staking_tier,
|
||||
"fee_model": fee_model,
|
||||
"tier_name": PERPS_TIERS[vip_tier]["name"],
|
||||
"staking_name": STAKING_TIERS.get(staking_tier, STAKING_TIERS["none"])["name"],
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Strategy-specific defaults
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
STRATEGY_FEE_MODELS = {
|
||||
"Order Book Imbalance": "taker",
|
||||
"Iceberg Detection": "taker",
|
||||
@@ -88,4 +185,23 @@ STRATEGY_FEE_MODELS = {
|
||||
"Cartea-Jaimungal": "maker",
|
||||
"Queue Imbalance": "taker",
|
||||
"Guéant Market Making": "maker",
|
||||
|
||||
# VBT strategy names
|
||||
"pairs": "taker",
|
||||
"hurst_vpin": "taker",
|
||||
"as_mm": "maker",
|
||||
"obi": "taker",
|
||||
"grid_mm": "maker",
|
||||
"composite_mm": "maker",
|
||||
"iceberg": "taker",
|
||||
"momentum": "taker",
|
||||
"mean_rev": "taker",
|
||||
}
|
||||
|
||||
|
||||
def get_strategy_fee_model(strategy_name: str) -> str:
|
||||
"""Get the default fee model for a strategy name."""
|
||||
for key, model in STRATEGY_FEE_MODELS.items():
|
||||
if key.lower() in strategy_name.lower():
|
||||
return model
|
||||
return "taker"
|
||||
|
||||
Reference in New Issue
Block a user