0446443d36
New strategies: - Cross-Sectional Momentum: long top-N, short bottom-N across HL universe - Spot-Perp Basis Arbitrage: delta-neutral spot vs perp price gap trading - Regime-Switching Ensemble: dynamically allocates strategies by market regime - Portfolio Construction: risk parity, vol targeting, correlation penalty Infrastructure: - DuckDBDataProvider: real tick/candle data for backtests (replaces synthetic) - Walk-Forward Validation: systematic IS/OOS across all 12 strategies - 3 Jupyter research notebooks (EDA, strategy research, portfolio) Pipeline integration: - deploy.py registry, sweep_runner, vbt_runner all updated - fee_tiers support for new strategies - All modules syntax-validated and import-tested
211 lines
10 KiB
Python
211 lines
10 KiB
Python
"""
|
|
Hyperliquid fee schedule — perps, spot, staking discounts, maker rebates.
|
|
|
|
Source: https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees
|
|
|
|
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 __future__ import annotations
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Perps fee tiers — base taker & maker rates (decimal)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
PERPS_TIERS = {
|
|
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 taker & maker rates (decimal)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
SPOT_TIERS = {
|
|
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 tiers — discount multiplier on all fees
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
STAKING_TIERS = {
|
|
"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:
|
|
"""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",
|
|
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"]
|
|
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 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)
|
|
|
|
|
|
def fee_tier_from_volume(volume_14d: float, market: str = "perps") -> int:
|
|
"""Determine fee tier from 14-day rolling volume."""
|
|
tiers = PERPS_TIERS if market == "perps" else SPOT_TIERS
|
|
current = 0
|
|
for t in sorted(tiers.keys()):
|
|
if volume_14d >= tiers[t]["min_volume"]:
|
|
current = t
|
|
return current
|
|
|
|
|
|
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",
|
|
"Funding Rate Arb": "taker",
|
|
"Pairs Trading": "taker",
|
|
"Avellaneda-Stoikov": "maker",
|
|
"Momentum Breakout": "taker",
|
|
"Mean Reversion": "taker",
|
|
"Hawkes OFI": "taker",
|
|
"Deep LOB": "maker",
|
|
"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",
|
|
"cross_sectional": "taker",
|
|
"spot_perp_basis": "taker",
|
|
"regime_ensemble": "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"
|