Files
ftdt-quant-lab/backtests/run.py
T
ramseshk 0c0d2124ad 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.
2026-08-04 07:26:40 +00:00

118 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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()