Add Hyperliquid fee tier selector — 7 VIP levels × 7 staking tiers
config/fee_tiers.py: complete Hyperliquid fee schedule with perps and spot
base rates plus staking discount multipliers. effective_rate() computes
the actual fee after staking discount. get_perp_fees() returns the
effective rate for a given VIP tier, staking tier, and fee model.
Backtest runner: added --fee-tier (0-6) and --staking-tier flags.
Regenerated all 12 backtests at VIP 0 baseline. Runner now shows fee tier
info at startup.
Server: /api/backtest/{name}/recalc endpoint accepts ?fee_tier=X&staking_tier=Y
and returns recalculated PnL with the new fee structure. On-the-fly
recalculation — no need to re-run the backtest.
Dashboard: VIP tier dropdown (VIP 0-6) and staking tier dropdown
(None/Wood/Bronze/Silver/Gold/Platinum/Diamond) in backtest detail panel.
Changing either instantly recalculates PnL via the API.
Key finding: Cartea-Jaimungal goes from -5.58% net at VIP0 to +2.39% net
at VIP6+Diamond (maker rebate: exchange pays YOU -0.0024% to provide
liquidity). Fee structure completely changes strategy viability assessment.
This commit is contained in:
@@ -25,6 +25,9 @@ from typing import Optional
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
import sys
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
from config.fee_tiers import get_perp_fees, PERPS_TIERS, STAKING_TIERS, STRATEGY_FEE_MODELS
|
||||
import uvicorn
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
@@ -195,6 +198,65 @@ async def get_backtest(name: str):
|
||||
return JSONResponse({"error": "not found"}, status_code=404)
|
||||
|
||||
|
||||
@app.get("/api/backtest/{name}/recalc")
|
||||
async def recalc_backtest(name: str, fee_tier: int = 0, staking_tier: str = "none"):
|
||||
"""Recalculate backtest PnL with different fee tier."""
|
||||
fpath = os.path.join(BACKTEST_DIR, f"{name}.json")
|
||||
if not os.path.exists(fpath):
|
||||
return JSONResponse({"error": "not found"}, status_code=404)
|
||||
with open(fpath) as f:
|
||||
data = json.load(f)
|
||||
|
||||
fee_model = STRATEGY_FEE_MODELS.get(data.get("strategy", ""), "taker")
|
||||
new_fee_rate = get_perp_fees(fee_tier, staking_tier, fee_model)
|
||||
|
||||
# Get original gross PnL and trades
|
||||
pnl_gross = data.get("pnl_gross", data.get("pnl", 0))
|
||||
trades = data.get("trades", [])
|
||||
|
||||
# Recalculate fees with new rate
|
||||
new_fees = 0.0
|
||||
new_trades = []
|
||||
for t in trades:
|
||||
sz = t.get("size", 0)
|
||||
px = t.get("price", 0)
|
||||
orig_fee = t.get("fee", 0)
|
||||
new_fee = sz * px * new_fee_rate * 2 # entry + exit
|
||||
new_fees += new_fee
|
||||
new_trades.append({**t, "fee": round(new_fee, 6),
|
||||
"pnl_net": round(t.get("pnl_gross", t.get("pnl", 0)) - new_fee, 4)})
|
||||
|
||||
new_pnl_net = pnl_gross - new_fees
|
||||
new_pnl_pct = new_pnl_net
|
||||
|
||||
ft = PERPS_TIERS.get(fee_tier, PERPS_TIERS[0])
|
||||
st = STAKING_TIERS.get(staking_tier, STAKING_TIERS["none"])
|
||||
eff_taker = get_perp_fees(fee_tier, staking_tier, "taker")
|
||||
eff_maker = get_perp_fees(fee_tier, staking_tier, "maker")
|
||||
|
||||
return JSONResponse({
|
||||
"strategy": data.get("strategy"),
|
||||
"fee_tier": ft["name"],
|
||||
"staking_tier": st["name"],
|
||||
"effective_taker_pct": round(eff_taker * 100, 4),
|
||||
"effective_maker_pct": round(eff_maker * 100, 4),
|
||||
"fee_model": fee_model,
|
||||
"pnl_gross": round(pnl_gross, 4),
|
||||
"pnl_gross_pct": round(pnl_gross, 4),
|
||||
"pnl_net": round(new_pnl_net, 4),
|
||||
"pnl_net_pct": round(new_pnl_pct, 4),
|
||||
"fees_total": round(new_fees, 4),
|
||||
"total_trades": len(new_trades),
|
||||
"equity_curve": data.get("equity_curve", []),
|
||||
"trades": new_trades[-100:],
|
||||
"sharpe": data.get("sharpe", 0),
|
||||
"sortino": data.get("sortino", 0),
|
||||
"max_dd": data.get("max_dd", 0),
|
||||
"win_rate": data.get("win_rate", 0),
|
||||
"num_periods": data.get("num_periods", 720),
|
||||
})
|
||||
|
||||
|
||||
@app.get("/api/backtest/{name}/csv")
|
||||
async def get_backtest_csv(name: str):
|
||||
"""Download backtest trades as CSV."""
|
||||
|
||||
Reference in New Issue
Block a user