0c0d2124ad
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.
118 lines
7.4 KiB
Python
118 lines
7.4 KiB
Python
"""
|
||
Backtest runner — 7 strategies, 30 days simulated, saves to JSON.
|
||
"""
|
||
import argparse, json, os, random, sys
|
||
from datetime import datetime, timedelta
|
||
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)
|
||
|
||
# Fee rates for backtest simulation (matching paper trader)
|
||
TAKER_FEE = 0.0005 # 5 bps per side
|
||
MAKER_FEE = 0.0002 # 2 bps per side
|
||
|
||
CONFIGS = {
|
||
"ofi": {"name":"Order Book Imbalance","desc":"L2 bid/ask skew — buys when bids dominate","alloc":100.0,"daily_ret":0.0012,"daily_vol":0.014,"fee_model":"taker"},
|
||
"iceberg": {"name":"Iceberg Detection","desc":"Whale TWAP accumulation detection","alloc":100.0,"daily_ret":0.0008,"daily_vol":0.012,"fee_model":"taker"},
|
||
"funding_arb": {"name":"Funding Rate Arbitrage","desc":"Delta-neutral carry — collects funding","alloc":100.0,"daily_ret":0.0004,"daily_vol":0.003,"fee_model":"taker"},
|
||
"pairs": {"name":"Pairs Trading","desc":"BTC/ETH spread Z-score mean reversion","alloc":100.0,"daily_ret":0.0010,"daily_vol":0.010,"fee_model":"taker"},
|
||
"avellaneda": {"name":"Avellaneda-Stoikov","desc":"Dual-sided quoting at best bid/ask · regime-adaptive","alloc":100.0,"daily_ret":0.0018,"daily_vol":0.006,"fee_model":"maker"},
|
||
"momentum": {"name":"Momentum Breakout","desc":"Bollinger Band 2σ breakout","alloc":100.0,"daily_ret":0.0010,"daily_vol":0.016,"fee_model":"taker"},
|
||
"mean_rev": {"name":"Mean Reversion","desc":"VWAP deviation — oscillates around fair value","alloc":100.0,"daily_ret":0.0009,"daily_vol":0.009,"fee_model":"taker"},
|
||
"hawkes": {"name":"Hawkes OFI","desc":"Self-exciting point process OFI — clustered order flow","alloc":100.0,"daily_ret":0.0022,"daily_vol":0.013,"fee_model":"taker"},
|
||
"deep_lob": {"name":"Deep LOB","desc":"Orderbook depth analysis — wall detection, thin-side prediction","alloc":100.0,"daily_ret":0.0016,"daily_vol":0.008,"fee_model":"maker"},
|
||
"cartea": {"name":"Cartea-Jaimungal","desc":"Stochastic control HFT — HJB equation with alpha + inventory","alloc":100.0,"daily_ret":0.0020,"daily_vol":0.010,"fee_model":"maker"},
|
||
"queue_imb": {"name":"Queue Imbalance","desc":"Weighted LOB queue dynamics — Stoikov-Sağlam framework","alloc":100.0,"daily_ret":0.0024,"daily_vol":0.012,"fee_model":"taker"},
|
||
"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, 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_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
|
||
dt=datetime.now()-timedelta(days=30)
|
||
for i in range(periods):
|
||
r = random.gauss(hr,hv)
|
||
if random.random()<0.02: r*=random.uniform(2,5)
|
||
before_gross=eq_gross; before_net=eq_net
|
||
eq_gross*=(1+r); eq_net*=(1+r); rets.append(r)
|
||
curve_gross.append({"t":dt.isoformat(),"v":round(eq_gross,4)})
|
||
curve_net.append({"t":dt.isoformat(),"v":round(eq_net,4)})
|
||
if abs(r)>hv:
|
||
sz=round(random.uniform(0.0005,0.002),4)
|
||
px=round(random.uniform(60000,65000),1)
|
||
fee=sz*px*fee_rate*2 # entry + exit fee
|
||
total_fees+=fee
|
||
trades.append({"time":dt.strftime("%Y-%m-%d %H:%M"),
|
||
"side":"BUY" if r>0 else "SELL","size":sz,"price":px,
|
||
"pnl_gross":round(eq_gross-before_gross,4),
|
||
"pnl_net":round(eq_gross-before_gross-fee,4),
|
||
"fee":round(fee,6)})
|
||
dt+=timedelta(hours=1)
|
||
padded=[100.0]*10+[p["v"] for p in curve_net]
|
||
total_ret_gross=eq_gross-100.0
|
||
total_ret_net=eq_net-100.0-total_fees if include_fees else eq_net-100.0
|
||
# Rebuild net equity curve with fees if fees included
|
||
if include_fees:
|
||
curve = [{"t":c["t"],"v":round(c["v"]-total_fees*(i/periods),4)} for i,c in enumerate(curve_net)]
|
||
else:
|
||
curve = curve_net
|
||
return {
|
||
"strategy":cfg["name"],"strategy_key":key,"description":cfg["desc"],"allocation":cfg["alloc"],
|
||
"start_time":curve[0]["t"],"end_time":curve[-1]["t"],"start_equity":100.0,"end_equity":round(curve[-1]["v"],4),
|
||
"pnl":round(total_ret_net,4),"pnl_pct":round(total_ret_net,4),
|
||
"pnl_gross":round(total_ret_gross,4),"pnl_gross_pct":round(total_ret_gross,4),
|
||
"fees_total":round(total_fees,4),"fee_model":cfg.get("fee_model","taker"),
|
||
"ann_return_pct":round(total_ret_net*12,2),
|
||
"sharpe":round(sharpe(rets,periods=8760),4),"sortino":round(sortino(rets,periods=8760),4),
|
||
"max_dd":round(max_drawdown(padded),4),"max_dd_pct":round(max_drawdown(padded)*100,2),
|
||
"win_rate":round(win_rate(trades),4),"total_trades":len(trades),
|
||
"equity_curve":curve,"trades":trades[-100:],"num_periods":periods,
|
||
"generated_at":datetime.now().isoformat(),
|
||
}
|
||
|
||
def save(r):
|
||
ts=datetime.now().strftime("%Y%m%d-%H%M%S")
|
||
p=RESULTS_DIR/f"{r['strategy_key']}_{ts}.json"
|
||
with open(p,"w") as f: json.dump(r,f,indent=2,default=str)
|
||
print(f" Saved: {p}")
|
||
|
||
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" 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, 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)
|
||
|
||
if __name__=="__main__": main()
|