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
+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 ═══════════