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:
+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."""
|
||||
|
||||
Reference in New Issue
Block a user