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:
ramseshk
2026-08-04 07:26:40 +00:00
parent ecfdd56d8f
commit 0c0d2124ad
16 changed files with 45910 additions and 6 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+13 -4
View File
@@ -7,6 +7,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from common.metrics import sharpe, sortino, max_drawdown, win_rate
from config.fee_tiers import get_perp_fees, PERPS_TIERS, STAKING_TIERS
RESULTS_DIR = Path(__file__).resolve().parent / "results"
os.makedirs(RESULTS_DIR, exist_ok=True)
@@ -30,14 +31,15 @@ CONFIGS = {
"gueant": {"name":"Guéant Market Making","desc":"Closed-form asymptotic MM — adverse selection handling","alloc":100.0,"daily_ret":0.0018,"daily_vol":0.005,"fee_model":"maker"},
}
def simulate(key, periods=720, include_fees=True):
def simulate(key, periods=720, include_fees=True, fee_tier=0, staking_tier="none"):
# Deterministic seed per strategy (hash() is randomized per Python process)
_fixed_seeds = {"ofi":42,"iceberg":43,"funding_arb":44,"pairs":45,"avellaneda":46,
"momentum":47,"mean_rev":48,"hawkes":49,"deep_lob":50,
"cartea":51,"queue_imb":52,"gueant":53}
random.seed(_fixed_seeds.get(key, 42))
cfg = CONFIGS[key]
fee_rate = MAKER_FEE if cfg.get("fee_model") == "maker" else TAKER_FEE
fee_model = cfg.get("fee_model", "taker")
fee_rate = get_perp_fees(fee_tier, staking_tier, fee_model)
hr = cfg["daily_ret"]/24; hv = cfg["daily_vol"]/(24**0.5)
eq_gross=100.0; eq_net=100.0; curve_gross=[]; curve_net=[]; rets=[]; trades=[]
total_fees=0.0
@@ -92,16 +94,23 @@ def main():
p=argparse.ArgumentParser()
p.add_argument("--strategy","-s",choices=list(CONFIGS)+["all"],default="all")
p.add_argument("--no-fees",action="store_true",help="Exclude simulated fees from PnL")
p.add_argument("--fee-tier",type=int,default=0,choices=range(7),help="VIP fee tier 0-6 (default: 0)")
p.add_argument("--staking-tier",default="none",choices=list(STAKING_TIERS.keys()),help="Staking discount tier (default: none)")
a=p.parse_args()
keys=list(CONFIGS) if a.strategy=="all" else [a.strategy]
include_fees=not a.no_fees
ft_info = PERPS_TIERS[a.fee_tier]
st_info = STAKING_TIERS[a.staking_tier]
print("="*60)
print(f" FTDT Quant Lab — Backtest Runner ({len(keys)} strategies)")
print(f" Fees: {'INCLUDED (default)' if include_fees else 'EXCLUDED (--no-fees)'}")
print(f" Fee Tier: {ft_info['name']} (taker={ft_info['taker']*100:.3f}%, maker={ft_info['maker']*100:.3f}%)")
print(f" Staking: {st_info['name']} ({st_info['multiplier']*100:.0f}% multiplier)")
print(f" Effective taker: {get_perp_fees(a.fee_tier,a.staking_tier,'taker')*100:.4f}%")
print(f" Effective maker: {get_perp_fees(a.fee_tier,a.staking_tier,'maker')*100:.4f}%")
print("="*60)
for k in keys:
cfg=CONFIGS[k]; print(f"\n Running: {cfg['name']}...")
r=simulate(k, include_fees=include_fees); save(r)
r=simulate(k, include_fees=include_fees, fee_tier=a.fee_tier, staking_tier=a.staking_tier); save(r)
print(f" Net PnL: {r['pnl_pct']:+.2f}% | Gross: {r['pnl_gross_pct']:+.2f}% | Fees: ${r['fees_total']:.2f} | Sharpe: {r['sharpe']:.2f} | Win: {r['win_rate']:.0%}")
print("\n"+"="*60); print(" Results in backtests/results/"); print(" View at: https://ftdt.io/cv (Backtest tab)"); print("="*60)
+91
View File
@@ -0,0 +1,91 @@
"""
Hyperliquid fee tiers — perps and spot, base rates + staking discounts.
Source: https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees
Fee = base_rate × staking_multiplier
Staking tiers are based on staked HYPE tokens.
"""
from dataclasses import dataclass, field
from typing import Optional
# ── Perps fee tiers (base rates) ──
PERPS_TIERS = {
0: {"name": "VIP 0", "volume": 0, "taker": 0.00045, "maker": 0.00015},
1: {"name": "VIP 1", "volume": 5_000_000, "taker": 0.00040, "maker": 0.00012},
2: {"name": "VIP 2", "volume": 25_000_000, "taker": 0.00035, "maker": 0.00008},
3: {"name": "VIP 3", "volume": 100_000_000,"taker": 0.00030, "maker": 0.00004},
4: {"name": "VIP 4", "volume": 250_000_000,"taker": 0.00025, "maker": 0.00000},
5: {"name": "VIP 5", "volume": 750_000_000,"taker": 0.00020, "maker": -0.00002},
6: {"name": "VIP 6", "volume": 2_500_000_000,"taker":0.00015,"maker": -0.00004},
}
# ── Spot fee tiers (base rates) ──
SPOT_TIERS = {
0: {"name": "VIP 0", "volume": 0, "taker": 0.00070, "maker": 0.00040},
1: {"name": "VIP 1", "volume": 100_000, "taker": 0.00060, "maker": 0.00030},
2: {"name": "VIP 2", "volume": 1_000_000, "taker": 0.00050, "maker": 0.00020},
3: {"name": "VIP 3", "volume": 10_000_000, "taker": 0.00040, "maker": 0.00010},
4: {"name": "VIP 4", "volume": 50_000_000, "taker": 0.00030, "maker": 0.00005},
5: {"name": "VIP 5", "volume": 200_000_000, "taker": 0.00020, "maker": 0.00000},
6: {"name": "VIP 6", "volume": 1_000_000_000,"taker":0.00010,"maker": -0.00005},
}
# ── Staking discount multipliers ──
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},
}
def effective_rate(base_rate: float, staking_tier: str = "none") -> float:
"""Calculate effective fee rate after staking discount."""
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."""
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)
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."""
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]["volume"]:
current = t
return current
# ── Strategy-specific defaults (matching existing classification) ──
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",
}
+62
View File
@@ -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."""
+36 -2
View File
@@ -126,10 +126,28 @@ footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)}
<div class="detail-panel" id="detail-panel">
<div class="detail-header">
<h2 id="det-name">Strategy Detail</h2>
<div style="display:flex;align-items:center;gap:10px">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<label id="fee-toggle-wrap" style="display:none;font-size:11px;color:var(--tx);cursor:pointer;user-select:none">
<input type="checkbox" id="fee-toggle" checked onchange="toggleFees()" style="cursor:pointer;margin-right:4px">Inc. fees
</label>
<select id="fee-tier-sel" style="display:none;font-size:10px;background:var(--srf);color:var(--hi);border:1px solid var(--ln);border-radius:4px;padding:3px 6px;font-family:var(--f)" onchange="onFeeTierChange()">
<option value="0">VIP 0 (0.045/0.015%)</option>
<option value="1">VIP 1 (0.040/0.012%)</option>
<option value="2">VIP 2 (0.035/0.008%)</option>
<option value="3">VIP 3 (0.030/0.004%)</option>
<option value="4">VIP 4 (0.025/0.000%)</option>
<option value="5">VIP 5 (0.020/-0.002%)</option>
<option value="6">VIP 6 (0.015/-0.004%)</option>
</select>
<select id="stake-tier-sel" style="display:none;font-size:10px;background:var(--srf);color:var(--hi);border:1px solid var(--ln);border-radius:4px;padding:3px 6px;font-family:var(--f)" onchange="onFeeTierChange()">
<option value="none">No Stake</option>
<option value="wood">Wood (×0.95)</option>
<option value="bronze">Bronze (×0.90)</option>
<option value="silver">Silver (×0.85)</option>
<option value="gold">Gold (×0.80)</option>
<option value="platinum">Platinum (×0.70)</option>
<option value="diamond">Diamond (×0.60)</option>
</select>
<a id="dl-csv" href="#" style="display:none;font-size:11px;color:var(--bl);text-decoration:none;padding:4px 10px;border:1px solid var(--ln);border-radius:5px" download>↓ CSV</a>
<button class="close-btn" onclick="closeDetail()">✕ Close</button>
</div>
@@ -205,10 +223,23 @@ function renCards(sgridId,ss,baseEq,tab,statsRowId){
}
// ═══════════ Fee toggle ═══════════
var currentBTName=null;
function toggleFees(){
feeOn=document.getElementById('fee-toggle').checked;
if(lastBTFull){renderBTDetail(lastBTFull)}
}
function onFeeTierChange(){
if(!currentBTName)return;
var ft=document.getElementById('fee-tier-sel').value;
var st=document.getElementById('stake-tier-sel').value;
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--tx);padding:20px">Recalculating with '+document.getElementById('fee-tier-sel').selectedOptions[0].text+'…</td></tr>';
fetch('/cv/api/backtest/'+encodeURIComponent(currentBTName)+'/recalc?fee_tier='+ft+'&staking_tier='+st)
.then(function(r){return r.json()}).then(function(full){
lastBTFull=full; renderBTDetail(full);
}).catch(function(e){
document.getElementById('det-trades').innerHTML='<tr><td colspan="7" style="text-align:center;color:var(--rd);padding:20px">Recalc failed: '+e.message+'</td></tr>';
});
}
// ═══════════ Render backtest detail with fee toggle ──
function renderBTDetail(full){
@@ -255,8 +286,11 @@ function openDetail(name,tab){
ss=lastData.strategies||{};
} else if(tab==='backtest'&&lastBT&&lastBT[name]){
var b=lastBT[name];
currentBTName=b.name;
document.getElementById('fee-toggle-wrap').style.display='inline';
document.getElementById('fee-toggle').checked=true; feeOn=true;
document.getElementById('fee-tier-sel').style.display='inline';
document.getElementById('stake-tier-sel').style.display='inline';
document.getElementById('dl-csv').style.display='inline';
document.getElementById('dl-csv').href='/cv/api/backtest/'+encodeURIComponent(b.name)+'/csv';
document.getElementById('det-desc').textContent='';
@@ -305,7 +339,7 @@ function openDetail(name,tab){
setTimeout(function(){if(detChart){detChart.applyOptions({width:document.getElementById('det-chart').offsetWidth,height:280});detChart.timeScale().fitContent()}},300);
}
function closeDetail(){document.getElementById('detail-overlay').classList.remove('on');document.getElementById('fee-toggle-wrap').style.display='none';document.getElementById('dl-csv').style.display='none';lastBTFull=null}
function closeDetail(){document.getElementById('detail-overlay').classList.remove('on');document.getElementById('fee-toggle-wrap').style.display='none';document.getElementById('fee-tier-sel').style.display='none';document.getElementById('stake-tier-sel').style.display='none';document.getElementById('dl-csv').style.display='none';lastBTFull=null;currentBTName=null}
document.addEventListener('keydown',function(e){if(e.key==='Escape')closeDetail()});
// ═══════════ WebSocket + render ═══════════