Add fee-toggle for backtests, CSV trade download, fee simulation in runner

Backtest runner: added per-trade fee simulation (maker 2bps, taker 5bps).
Each trade now records pnl_gross, pnl_net, and fee. New --no-fees flag
excludes fees from PnL. Output includes pnl_gross/pnl_gross_pct and
fees_total alongside existing pnl (net). Regenerated all 12 backtests.

Server: added /api/backtest/{name}/csv endpoint — returns trades as CSV
with columns time,side,size,price,pnl_gross,pnl_net,fee.
Content-Disposition: attachment triggers browser download.

Dashboard: added "Inc. fees" checkbox toggle in backtest detail panel.
Unchecking shows gross PnL (before fees). "↓ CSV" button downloads
the trade history. Both hidden when detail is closed.
This commit is contained in:
ramseshk
2026-08-04 07:16:41 +00:00
parent e4de21192a
commit ecfdd56d8f
15 changed files with 45829 additions and 38 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+42 -12
View File
@@ -11,6 +11,10 @@ from common.metrics import sharpe, sortino, max_drawdown, win_rate
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"},
@@ -26,30 +30,51 @@ CONFIGS = {
"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):
def simulate(key, periods=720, include_fees=True):
# 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_rate = MAKER_FEE if cfg.get("fee_model") == "maker" else TAKER_FEE
hr = cfg["daily_ret"]/24; hv = cfg["daily_vol"]/(24**0.5)
eq=100.0; curve=[]; rets=[]; trades=[]
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=eq; eq*=(1+r); rets.append(r)
curve.append({"t":dt.isoformat(),"v":round(eq,4)})
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:
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)})
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]
total_ret=eq-100.0
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(eq,4),
"pnl":round(total_ret,4),"pnl_pct":round(total_ret,4),"ann_return_pct":round(total_ret*12,2),
"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),
@@ -66,13 +91,18 @@ def save(r):
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")
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)
include_fees=not a.no_fees
print("="*60)
print(f" FTDT Quant Lab — Backtest Runner ({len(keys)} strategies)")
print(f" Fees: {'INCLUDED (default)' if include_fees else 'EXCLUDED (--no-fees)'}")
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%}")
r=simulate(k, include_fees=include_fees); 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()