Rename VIP→Tier, recalculate equity curve on fee tier change

Renamed all "VIP N" to "Tier N" in config, server, and HTML dropdowns.

Added recalc_equity_curve() that rebuilds the equity curve with new
fee rates. Previously the equity curve was passed through unchanged
when switching tiers, so the chart visually stayed identical even
though PnL numbers changed. Now each tier produces its own curve.

Example OBI historical: Tier 0 equity ends at 141.5, Tier 6 at 149.7 —
the chart visibly shifts up as fees drop from 4.5bp to 2.4bp taker.
This commit is contained in:
ramseshk
2026-08-04 09:12:07 +00:00
parent 055da97a79
commit 526a40b01e
3 changed files with 55 additions and 22 deletions
+14 -14
View File
@@ -12,24 +12,24 @@ from typing import Optional
# ── Perps fee tiers (base rates) ── # ── Perps fee tiers (base rates) ──
PERPS_TIERS = { PERPS_TIERS = {
0: {"name": "VIP 0", "volume": 0, "taker": 0.00045, "maker": 0.00015}, 0: {"name": "Tier 0", "volume": 0, "taker": 0.00045, "maker": 0.00015},
1: {"name": "VIP 1", "volume": 5_000_000, "taker": 0.00040, "maker": 0.00012}, 1: {"name": "Tier 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}, 2: {"name": "Tier 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}, 3: {"name": "Tier 3", "volume": 100_000_000,"taker": 0.00030, "maker": 0.00004},
4: {"name": "VIP 4", "volume": 500_000_000, "taker": 0.00028, "maker": 0.00000}, 4: {"name": "Tier 4", "volume": 500_000_000, "taker": 0.00028, "maker": 0.00000},
5: {"name": "VIP 5", "volume": 2_000_000_000, "taker": 0.00026, "maker": 0.00000}, 5: {"name": "Tier 5", "volume": 2_000_000_000, "taker": 0.00026, "maker": 0.00000},
6: {"name": "VIP 6", "volume": 7_000_000_000, "taker": 0.00024, "maker": 0.00000}, 6: {"name": "Tier 6", "volume": 7_000_000_000, "taker": 0.00024, "maker": 0.00000},
} }
# ── Spot fee tiers (base rates) ── # ── Spot fee tiers (base rates) ──
SPOT_TIERS = { SPOT_TIERS = {
0: {"name": "VIP 0", "volume": 0, "taker": 0.00070, "maker": 0.00040}, 0: {"name": "Tier 0", "volume": 0, "taker": 0.00070, "maker": 0.00040},
1: {"name": "VIP 1", "volume": 100_000, "taker": 0.00060, "maker": 0.00030}, 1: {"name": "Tier 1", "volume": 100_000, "taker": 0.00060, "maker": 0.00030},
2: {"name": "VIP 2", "volume": 1_000_000, "taker": 0.00050, "maker": 0.00020}, 2: {"name": "Tier 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}, 3: {"name": "Tier 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}, 4: {"name": "Tier 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}, 5: {"name": "Tier 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}, 6: {"name": "Tier 6", "volume": 1_000_000_000,"taker":0.00010,"maker": -0.00005},
} }
# ── Staking discount multipliers ── # ── Staking discount multipliers ──
+34 -1
View File
@@ -200,6 +200,34 @@ async def get_backtest(name: str):
return JSONResponse({"error": "not found"}, status_code=404) return JSONResponse({"error": "not found"}, status_code=404)
def recalc_equity_curve(equity_curve, trades, new_fee_rate, fee_model):
"""Rebuild equity curve with new fee rates, preserving gross PnL."""
if not equity_curve or not trades:
return equity_curve
fee_deltas = {}
cum_delta = 0.0
for t in trades:
sz = t.get("size", 0)
px = t.get("price", 0)
old_fee = t.get("fee", 0)
new_fee = sz * px * new_fee_rate * 2
delta = old_fee - new_fee
cum_delta += delta
fee_deltas[t.get("time", "")] = cum_delta
new_curve = []
delta_idx = 0
trade_times = list(fee_deltas.keys())
cum = 0.0
for pt in equity_curve:
pt_time = pt.get("t", "")
while delta_idx < len(trade_times) and trade_times[delta_idx] <= pt_time:
cum = fee_deltas[trade_times[delta_idx]]
delta_idx += 1
new_curve.append({"t": pt_time, "v": round(pt.get("v", 0) + cum, 6)})
return new_curve
@app.get("/api/backtest/{name}/recalc") @app.get("/api/backtest/{name}/recalc")
async def recalc_backtest(name: str, fee_tier: int = 0, staking_tier: str = "none"): async def recalc_backtest(name: str, fee_tier: int = 0, staking_tier: str = "none"):
"""Recalculate backtest PnL with different fee tier.""" """Recalculate backtest PnL with different fee tier."""
@@ -251,7 +279,12 @@ async def recalc_backtest(name: str, fee_tier: int = 0, staking_tier: str = "non
"pnl_net_pct": round(new_pnl_pct, 4), "pnl_net_pct": round(new_pnl_pct, 4),
"fees_total": round(new_fees, 4), "fees_total": round(new_fees, 4),
"total_trades": len(new_trades), "total_trades": len(new_trades),
"equity_curve": data.get("equity_curve", []), "equity_curve": recalc_equity_curve(
data.get("equity_curve", []),
data.get("trades", []),
new_fee_rate,
fee_model
),
"trades": new_trades[-100:], "trades": new_trades[-100:],
"sharpe": data.get("sharpe", 0), "sharpe": data.get("sharpe", 0),
"sortino": data.get("sortino", 0), "sortino": data.get("sortino", 0),
+7 -7
View File
@@ -162,13 +162,13 @@ footer a{color:#3f3f4a;text-decoration:none}footer a:hover{color:var(--tx)}
<input type="checkbox" id="fee-toggle" checked onchange="toggleFees()" style="cursor:pointer;margin-right:4px">Inc. fees <input type="checkbox" id="fee-toggle" checked onchange="toggleFees()" style="cursor:pointer;margin-right:4px">Inc. fees
</label> </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()"> <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="0">Tier 0 (0.045/0.015%)</option>
<option value="1">VIP 1 (0.040/0.012%)</option> <option value="1">Tier 1 (0.040/0.012%)</option>
<option value="2">VIP 2 (0.035/0.008%)</option> <option value="2">Tier 2 (0.035/0.008%)</option>
<option value="3">VIP 3 (0.030/0.004%)</option> <option value="3">Tier 3 (0.030/0.004%)</option>
<option value="4">VIP 4 (0.028/0.000%)</option> <option value="4">Tier 4 (0.028/0.000%)</option>
<option value="5">VIP 5 (0.026/0.000%)</option> <option value="5">Tier 5 (0.026/0.000%)</option>
<option value="6">VIP 6 (0.024/0.000%)</option> <option value="6">Tier 6 (0.024/0.000%)</option>
</select> </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()"> <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="none">No Stake</option>