Files
ftdt-quant-lab/backtests/run.py
T
ramseshk e4de21192a Fix dashboard backtest detail, deterministic backtest seeds, paper trader fees, live node crash guard
Backtest detail: openDetail() now fetches full backtest JSON from the API
instead of showing "Full trade data not in summary". Renders equity curve
chart + full trade history table with 100 rows.

Backtest reproducibility: replaced hash(key) with fixed per-strategy seeds.
Python's hash() is randomized per process (PYTHONHASHSEED), causing wildly
different results for same strategy across runs. Now deterministic.

Server: added total_trades and sortino to /api/backtests summary response.

Paper trader: fixed Avellaneda-Stoikov simulate using TAKER_FEE instead of
MAKER_FEE. Lowered OBI signal threshold from 5bps to 1.5bps for flat markets.

Live node: added None-guard in get_mark_prices — Hyperliquid testnet API
sometimes returns null, crashing the node. Wrapped in try/except.
2026-08-04 07:07:15 +00:00

79 lines
5.1 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
RESULTS_DIR = Path(__file__).resolve().parent / "results"
os.makedirs(RESULTS_DIR, exist_ok=True)
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):
# 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]
hr = cfg["daily_ret"]/24; hv = cfg["daily_vol"]/(24**0.5)
eq=100.0; curve=[]; rets=[]; trades=[]
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=eq; eq*=(1+r); rets.append(r)
curve.append({"t":dt.isoformat(),"v":round(eq,4)})
if abs(r)>hv:
trades.append({"time":dt.strftime("%Y-%m-%d %H:%M"),"side":"BUY" if r>0 else "SELL","size":round(random.uniform(0.0005,0.002),4),"price":round(random.uniform(60000,65000),1),"pnl":round(eq-before,4)})
dt+=timedelta(hours=1)
padded=[100.0]*10+[p["v"] for p in curve]
total_ret=eq-100.0
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(eq,4),
"pnl":round(total_ret,4),"pnl_pct":round(total_ret,4),"ann_return_pct":round(total_ret*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")
a=p.parse_args()
keys=list(CONFIGS) if a.strategy=="all" else [a.strategy]
print("="*60); print(f" FTDT Quant Lab — Backtest Runner ({len(keys)} strategies)"); print("="*60)
for k in keys:
cfg=CONFIGS[k]; print(f"\n Running: {cfg['name']}...")
r=simulate(k); save(r)
print(f" PnL: {r['pnl_pct']:+.2f}% | Sharpe: {r['sharpe']:.2f} | DD: {r['max_dd_pct']:.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()