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:
|
||||
|
||||
+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"
|
||||
|
||||
+80
-10
@@ -580,18 +580,23 @@ def _normalize_vbt_fields(data: dict) -> dict:
|
||||
trades = out.get("trades", [])
|
||||
if trades:
|
||||
asset = _infer_asset(out.get("strategy", ""), "")
|
||||
hl_fee = 0.0005 # default Hyperliquid taker rate
|
||||
strategy_name = out.get("strategy", "")
|
||||
from config.fee_tiers import get_strategy_fee_model, compute_trade_fees
|
||||
fee_model = get_strategy_fee_model(strategy_name)
|
||||
for t in trades:
|
||||
if not t.get("asset"):
|
||||
t["asset"] = asset
|
||||
if not t.get("fee"):
|
||||
entry_px = float(t.get("entry_px", 0))
|
||||
exit_px = float(t.get("exit_px", 0))
|
||||
size = float(t.get("size", 0))
|
||||
notional_entry = size * entry_px
|
||||
notional_exit = size * exit_px
|
||||
t["fee"] = round((notional_entry + notional_exit) * hl_fee, 6)
|
||||
t["fee_rate"] = f"{hl_fee*100:.3f}%"
|
||||
ft = compute_trade_fees(
|
||||
side=t.get("side", "BUY"),
|
||||
size=float(t.get("size", 0)),
|
||||
entry_px=float(t.get("entry_px", 0)),
|
||||
exit_px=float(t.get("exit_px", 0)),
|
||||
vip_tier=0, staking_tier="none",
|
||||
fee_model=fee_model,
|
||||
)
|
||||
t["fee"] = ft["total_fee"]
|
||||
t["fee_rate_pct"] = ft["effective_rate_pct"]
|
||||
if not t.get("pnl_net"):
|
||||
t["pnl_net"] = t.get("pnl", 0)
|
||||
if not t.get("pnl_gross"):
|
||||
@@ -746,11 +751,16 @@ async def run_vbt_backtest(
|
||||
limit: int = 500,
|
||||
coin: str = "",
|
||||
testnet: bool = False,
|
||||
fee_tier: int = 0,
|
||||
staking_tier: str = "none",
|
||||
maker_rebate: int = 0,
|
||||
):
|
||||
"""Run a new VectorBT backtest and return results."""
|
||||
"""Run a new VectorBT backtest with Hyperliquid fee schedule."""
|
||||
try:
|
||||
from backtests.vbt_runner import VBTBacktestRunner
|
||||
runner = VBTBacktestRunner()
|
||||
runner = VBTBacktestRunner(
|
||||
vip_tier=fee_tier, staking_tier=staking_tier, maker_rebate_tier=maker_rebate
|
||||
)
|
||||
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
coin_suffix = f"_{coin}" if coin else ""
|
||||
result = runner.run_strategy(
|
||||
@@ -806,6 +816,66 @@ async def list_vbt_strategies():
|
||||
])
|
||||
|
||||
|
||||
@app.get("/api/vbt/result/{filename}/recalc")
|
||||
async def recalc_vbt_trades(
|
||||
filename: str,
|
||||
fee_tier: int = 0,
|
||||
staking_tier: str = "none",
|
||||
maker_rebate: int = 0,
|
||||
):
|
||||
"""Recalculate VBT trade fees with different fee tier/staking."""
|
||||
_refresh_vbt_cache()
|
||||
if filename in _vbt_full_cache:
|
||||
data = dict(_vbt_full_cache[filename])
|
||||
else:
|
||||
for d in [BACKTEST_DIR, HISTORICAL_DIR]:
|
||||
fpath = os.path.join(d, filename)
|
||||
if os.path.exists(fpath):
|
||||
with open(fpath) as f:
|
||||
data = json.load(f)
|
||||
break
|
||||
else:
|
||||
return JSONResponse({"error": "not found"}, status_code=404)
|
||||
|
||||
from config.fee_tiers import get_strategy_fee_model, compute_trade_fees
|
||||
fee_model = get_strategy_fee_model(data.get("strategy", ""))
|
||||
|
||||
trades = data.get("trades", [])
|
||||
for t in trades:
|
||||
ft = compute_trade_fees(
|
||||
side=t.get("side", "BUY"),
|
||||
size=float(t.get("size", 0)),
|
||||
entry_px=float(t.get("entry_px", 0)),
|
||||
exit_px=float(t.get("exit_px", 0)),
|
||||
vip_tier=fee_tier,
|
||||
staking_tier=staking_tier,
|
||||
fee_model=fee_model,
|
||||
maker_rebate_tier=maker_rebate,
|
||||
)
|
||||
t["fee"] = ft["total_fee"]
|
||||
t["fee_rate_pct"] = ft["effective_rate_pct"]
|
||||
pnl_old = float(t.get("pnl_gross", t.get("pnl", 0)))
|
||||
t["pnl_net"] = round(pnl_old - ft["total_fee"], 4)
|
||||
|
||||
data["fee_info"] = compute_trade_fees(
|
||||
"BUY", 0.001, 100000.0, 100000.0,
|
||||
vip_tier=fee_tier, staking_tier=staking_tier,
|
||||
fee_model=fee_model, maker_rebate_tier=maker_rebate,
|
||||
)
|
||||
return JSONResponse(data)
|
||||
|
||||
|
||||
@app.get("/api/vbt/fee_tiers")
|
||||
async def list_fee_tiers():
|
||||
"""Return current Hyperliquid fee schedule for frontend."""
|
||||
from config.fee_tiers import PERPS_TIERS, STAKING_TIERS, MAKER_REBATES
|
||||
return JSONResponse({
|
||||
"perps": {str(k): v for k, v in PERPS_TIERS.items()},
|
||||
"staking": {k: v for k, v in STAKING_TIERS.items()},
|
||||
"maker_rebates": {str(k): v for k, v in MAKER_REBATES.items()},
|
||||
})
|
||||
|
||||
|
||||
@app.get("/api/vbt/result/{filename}/csv")
|
||||
async def get_vbt_csv(filename: str):
|
||||
"""Download VBT backtest trades as CSV."""
|
||||
|
||||
@@ -118,6 +118,26 @@ select:focus{outline:none;border-color:#555}
|
||||
<option value="trades">Trades</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row" style="margin-top:2px">
|
||||
<select id="fee-tier" style="flex:0.6" onchange="recalcIfSelected()">
|
||||
<option value="0" selected>T0 (Base 0.045%/0.015%)</option>
|
||||
<option value="1">T1 (>$5M 0.040/0.012)</option>
|
||||
<option value="2">T2 (>$25M 0.035/0.008)</option>
|
||||
<option value="3">T3 (>$100M 0.030/0.004)</option>
|
||||
<option value="4">T4 (>$500M 0.028/0.000)</option>
|
||||
<option value="5">T5 (>$2B 0.026/0.000)</option>
|
||||
<option value="6">T6 (>$7B 0.024/0.000)</option>
|
||||
</select>
|
||||
<select id="staking-tier" style="flex:0.4" onchange="recalcIfSelected()">
|
||||
<option value="none">No Stake</option>
|
||||
<option value="wood">Wood (-5%)</option>
|
||||
<option value="bronze">Bronze (-10%)</option>
|
||||
<option value="silver">Silver (-15%)</option>
|
||||
<option value="gold">Gold (-20%)</option>
|
||||
<option value="platinum">Platinum (-30%)</option>
|
||||
<option value="diamond">Diamond (-40%)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<select id="run-strategy">
|
||||
<option value="pairs">Pairs Trading</option>
|
||||
@@ -259,7 +279,8 @@ function renderDetail(r){
|
||||
const assetBadge = t.asset ? '<span style="font-size:9px;color:#03A9F4;background:#0d1f2b;border:1px solid #1a3a4a;border-radius:2px;padding:0 4px;margin-left:4px">'+t.asset+'</span>' : '';
|
||||
return '<tr><td style="color:#777;font-size:10px">'+String(t.time||'').substring(0,19)+'</td><td class="'+(String(t.side||'').includes('BUY')?'pos':'neg')+'">'+String(t.side||'')+assetBadge+'</td><td>'+Number(t.size||0).toFixed(6)+'</td><td>$'+Number(t.entry_px||0).toFixed(1)+'</td><td>$'+Number(t.exit_px||0).toFixed(1)+'</td><td class="neg" style="font-size:10px">$'+fee.toFixed(6)+'</td><td class="'+pnlCls+'">$'+pnl_net.toFixed(4)+'</td><td style="font-size:10px">'+String(t.duration||'')+'</td></tr>';
|
||||
}).join('');
|
||||
trades_html='<div class="chart-box full"><div style="display:flex;justify-content:space-between;align-items:center"><h4>Trade Log ('+trades.length+' trades)</h4></div><div class="trade-scroll"><table class="trades-table"><thead><tr><th>Time</th><th>Side</th><th>Size</th><th>Entry</th><th>Exit</th><th>Fee</th><th>PnL (net)</th><th>Duration</th></tr></thead><tbody>'+rows+'</tbody></table></div></div>';
|
||||
const feeRate = trades[0]?.fee_rate_pct ? ' · '+trades[0].fee_rate_pct+'%' : '';
|
||||
trades_html='<div class="chart-box full"><div style="display:flex;justify-content:space-between;align-items:center"><h4>Trade Log ('+trades.length+' trades'+feeRate+')</h4></div><div class="trade-scroll"><table class="trades-table"><thead><tr><th>Time</th><th>Side</th><th>Size</th><th>Entry</th><th>Exit</th><th>Fee</th><th>PnL (net)</th><th>Duration</th></tr></thead><tbody>'+rows+'</tbody></table></div></div>';
|
||||
}
|
||||
|
||||
document.getElementById('content').innerHTML=
|
||||
@@ -307,11 +328,26 @@ function exportJSON(){if(!currentResult)return;const blob=new Blob([JSON.stringi
|
||||
|
||||
function exportCSV(){if(!currentFilename)return;const a=document.createElement('a');a.href=API+'/api/vbt/result/'+encodeURIComponent(currentFilename)+'/csv';a.download=currentFilename.replace('.json','')+'_trades.csv';a.click();}
|
||||
|
||||
async function recalcIfSelected(){
|
||||
if(!currentFilename)return;
|
||||
const feeTier=document.getElementById('fee-tier').value;
|
||||
const staking=document.getElementById('staking-tier').value;
|
||||
const makerRebate=0;
|
||||
try{
|
||||
const url=API+'/api/vbt/result/'+encodeURIComponent(currentFilename)+'/recalc?fee_tier='+feeTier+'&staking_tier='+staking+'&maker_rebate='+makerRebate;
|
||||
const r=await(await fetch(url)).json();
|
||||
if(r.error){return;}
|
||||
currentResult=r;renderDetail(r);
|
||||
}catch(e){/* ignore */}
|
||||
}
|
||||
|
||||
async function runNewBacktest(){
|
||||
const strat=document.getElementById('run-strategy').value;
|
||||
const interval=document.getElementById('run-interval').value;
|
||||
const limit=document.getElementById('run-limit').value;
|
||||
const coin=document.getElementById('run-coin').value;
|
||||
const feeTier=document.getElementById('fee-tier').value;
|
||||
const staking=document.getElementById('staking-tier').value;
|
||||
const btn=document.getElementById('btn-run');
|
||||
const status=document.getElementById('run-status');
|
||||
const orig=btn.textContent;btn.textContent='⏳ Running...';btn.disabled=true;
|
||||
@@ -322,7 +358,7 @@ async function runNewBacktest(){
|
||||
status.innerHTML='<div class="run-status">Running '+strat+' '+interval+'... '+elapsed+'s</div><div class="progress-bar" style="width:'+Math.min(elapsed*10,95)+'%"></div>';
|
||||
},1000);
|
||||
try{
|
||||
let url=API+'/api/vbt/run?strategy='+strat+'&interval='+interval+'&limit='+limit;
|
||||
let url=API+'/api/vbt/run?strategy='+strat+'&interval='+interval+'&limit='+limit+'&fee_tier='+feeTier+'&staking_tier='+staking;
|
||||
if(coin)url+='&coin='+coin;
|
||||
const resp=await fetch(url);
|
||||
const data=await resp.json();
|
||||
|
||||
Reference in New Issue
Block a user