mirror of
https://github.com/KhizarImran/backtestingfx.git
synced 2026-08-04 15:57:46 +00:00
bench: add three-way speed benchmark (backtesting.py vs py-next vs native rust)
Adds a native-Rust SMA strategy path (Engine.run_native_sma) so we can time the pure-Rust engine against the Python-callback path and backtesting.py.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Speed benchmark: backtesting.py vs backtestingfx, on the same SMA-cross strategy.
|
||||
|
||||
Three paths are timed:
|
||||
1. backtesting.py — everything in Python
|
||||
2. backtestingfx (py) — Rust engine + broker, but next() runs in Python each bar
|
||||
3. backtestingfx (rust) — SMA strategy runs entirely in Rust, no per-bar Python call
|
||||
|
||||
The dataset is small (2000 bars), so it's tiled up to BARS to give the engines
|
||||
real work. Timestamps are regenerated as a clean hourly range so backtesting.py
|
||||
gets the sorted DatetimeIndex it needs.
|
||||
|
||||
Note: paths 1 and 2 both pay the per-bar Python cost (the strategy logic), so the
|
||||
gap between them measures only the engine/broker plumbing. Path 3 shows the
|
||||
engine's ceiling when nothing crosses into Python.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
import pandas as pd
|
||||
from backtesting import Backtest as BtBacktest, Strategy as BtStrategy
|
||||
from backtestingfx import Backtest, Strategy
|
||||
from backtestingfx import _backtestingfx as _rust
|
||||
|
||||
FAST = 10
|
||||
SLOW = 50
|
||||
LOT = 0.1
|
||||
BARS = 50_000
|
||||
REPEATS = 2
|
||||
|
||||
|
||||
class SmaCrossBt(BtStrategy):
|
||||
def init(self):
|
||||
pass
|
||||
|
||||
def next(self):
|
||||
if len(self.data.Close) < SLOW:
|
||||
return
|
||||
fast_sma = self.data.Close[-FAST:].mean()
|
||||
slow_sma = self.data.Close[-SLOW:].mean()
|
||||
if not self.position:
|
||||
if fast_sma > slow_sma:
|
||||
self.buy()
|
||||
elif fast_sma < slow_sma:
|
||||
self.position.close()
|
||||
|
||||
|
||||
class SmaCrossFx(Strategy):
|
||||
def next(self):
|
||||
if self.index < SLOW:
|
||||
return
|
||||
closes = [b.close for b in self.data[-SLOW:]]
|
||||
fast_sma = sum(closes[-FAST:]) / FAST
|
||||
slow_sma = sum(closes) / SLOW
|
||||
if not self.positions:
|
||||
if fast_sma > slow_sma:
|
||||
self.buy(LOT)
|
||||
elif fast_sma < slow_sma:
|
||||
self.close_all()
|
||||
|
||||
|
||||
def load_tiled(n):
|
||||
"""Load the EURUSD CSV and tile it up to n rows with a fresh hourly index."""
|
||||
df = pd.read_csv("data/EURUSD_1H.csv")[["open", "high", "low", "close", "volume"]]
|
||||
reps = -(-n // len(df)) # ceil division
|
||||
df = pd.concat([df] * reps, ignore_index=True).iloc[:n].copy()
|
||||
df["timestamp"] = pd.date_range("2000-01-01", periods=n, freq="h")
|
||||
return df
|
||||
|
||||
|
||||
def best_of(fn, repeats=REPEATS):
|
||||
"""Run fn repeats times, return (best_seconds, result_of_first_run)."""
|
||||
best = float("inf")
|
||||
result = None
|
||||
for i in range(repeats):
|
||||
t0 = time.perf_counter()
|
||||
r = fn()
|
||||
dt = time.perf_counter() - t0
|
||||
if i == 0:
|
||||
result = r
|
||||
best = min(best, dt)
|
||||
return best, result
|
||||
|
||||
|
||||
df = load_tiled(BARS)
|
||||
|
||||
# backtesting.py wants a DatetimeIndex and capitalised columns
|
||||
df_bt = df.set_index("timestamp").rename(
|
||||
columns={"open": "Open", "high": "High", "low": "Low", "close": "Close", "volume": "Volume"}
|
||||
)
|
||||
|
||||
# native-rust path needs Bar objects built directly
|
||||
bars = [
|
||||
_rust.Bar(
|
||||
timestamp=int(ts.timestamp()),
|
||||
open=float(o), high=float(h), low=float(l), close=float(c), volume=float(v),
|
||||
)
|
||||
for ts, o, h, l, c, v in zip(
|
||||
df["timestamp"], df["open"], df["high"], df["low"], df["close"], df["volume"]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def run_bt():
|
||||
return BtBacktest(df_bt, SmaCrossBt, cash=10_000, commission=0).run()
|
||||
|
||||
|
||||
def run_fx_py():
|
||||
return Backtest(df, SmaCrossFx, cash=10_000, commission=0, spread=0).run()
|
||||
|
||||
|
||||
def run_fx_rust():
|
||||
engine = _rust.Engine(bars, 10_000.0, 0.0, 0.0, 100_000.0, 1.0)
|
||||
return engine.run_native_sma(FAST, SLOW, LOT)
|
||||
|
||||
|
||||
print(f"Benchmarking on {BARS:,} bars, best of {REPEATS} runs...\n", flush=True)
|
||||
|
||||
print(" timing backtesting.py ...", flush=True)
|
||||
t_bt, r_bt = best_of(run_bt)
|
||||
print(" timing backtestingfx (py next) ...", flush=True)
|
||||
t_py, r_py = best_of(run_fx_py)
|
||||
print(" timing backtestingfx (rust) ...\n", flush=True)
|
||||
t_rs, r_rs = best_of(run_fx_rust)
|
||||
|
||||
print("=" * 62)
|
||||
print(f"{'Path':<24}{'Time (s)':>10}{'Bars/sec':>14}{'Speedup':>12}")
|
||||
print("=" * 62)
|
||||
for name, t in [
|
||||
("backtesting.py", t_bt),
|
||||
("backtestingfx (py next)", t_py),
|
||||
("backtestingfx (rust)", t_rs),
|
||||
]:
|
||||
print(f"{name:<24}{t:>10.3f}{BARS / t:>14,.0f}{t_bt / t:>11.1f}x")
|
||||
print("=" * 62)
|
||||
|
||||
print("\nCorrectness (should roughly agree):")
|
||||
print(f"{'Path':<24}{'Trades':>10}{'Return %':>12}")
|
||||
print(f"{'backtesting.py':<24}{r_bt['# Trades']:>10}{r_bt['Return [%]']:>12.2f}")
|
||||
print(f"{'backtestingfx (py next)':<24}{r_py.num_trades:>10}{r_py.total_return_pct:>12.2f}")
|
||||
print(f"{'backtestingfx (rust)':<24}{r_rs.num_trades:>10}{r_rs.total_return_pct:>12.2f}")
|
||||
print(
|
||||
"\nNote: py-next vs rust measure the same engine; the gap is the per-bar\n"
|
||||
"Python callback cost. Both use fixed lots, so Return % differs from\n"
|
||||
"backtesting.py's cash-based sizing."
|
||||
)
|
||||
Reference in New Issue
Block a user