Files
backtestingfx/examples/compare_bt.py
T
KhizarImran c22153f02f feat: strategy data access, sharpe ratio, and correctness fixes
- Trade.pnl now stores net pnl (after exit commission) so per-trade stats are accurate
- Position pyclass uses from_py_object to fix deprecation warning
- Removed dead AttributeError swallow in engine.rs
- Strategy gains self.data, self.index, self.cash, self.equity properties
- Broker.cash exposed to Python via pyo3(get)
- Sharpe ratio added to Stats (unannualized)
- Added examples/sma_cross.py and examples/compare_bt.py
- Logic verified against backtesting.py: 34 trades, 29.4% win rate match
2026-07-05 14:42:10 +01:00

91 lines
3.5 KiB
Python

"""
Compares our SMA crossover logic against backtesting.py using the same data,
same SMA periods, and zero commission/spread so only entry/exit logic is tested.
Trade count and win rate should match between both libraries.
Dollar PnL will differ because the sizing models are different:
- backtesting.py: buys fractional units based on available cash
- backtestingfx: fixed lot size (0.1 lots = 10,000 units)
"""
import pandas as pd
from backtesting import Backtest as BtBacktest, Strategy as BtStrategy
from backtestingfx import Backtest, Strategy
FAST = 10
SLOW = 50
# ── backtesting.py ────────────────────────────────────────────────────────────
class SmaCrossBt(BtStrategy):
fast = FAST
slow = SLOW
def init(self):
pass
def next(self):
if len(self.data.Close) < self.slow:
return
fast_sma = self.data.Close[-self.fast:].mean()
slow_sma = self.data.Close[-self.slow:].mean()
if not self.position:
if fast_sma > slow_sma:
self.buy()
else:
if fast_sma < slow_sma:
self.position.close()
# ── backtestingfx ─────────────────────────────────────────────────────────────
class SmaCrossFx(Strategy):
fast = FAST
slow = SLOW
def next(self):
if self.index < self.slow:
return
closes = [b.close for b in self.data[-self.slow:]]
fast_sma = sum(closes[-self.fast:]) / self.fast
slow_sma = sum(closes) / self.slow
if not self.positions:
if fast_sma > slow_sma:
self.buy(0.1)
else:
if fast_sma < slow_sma:
self.close_all()
# ── run both ──────────────────────────────────────────────────────────────────
df_raw = pd.read_csv("data/EURUSD_1H.csv")
# backtesting.py needs a DatetimeIndex and capitalised column names
df_bt = df_raw.copy()
df_bt["timestamp"] = pd.to_datetime(df_bt["timestamp"], utc=True)
df_bt = df_bt.set_index("timestamp")
df_bt.index = df_bt.index.tz_localize(None)
df_bt = df_bt.rename(columns={"open": "Open", "high": "High", "low": "Low", "close": "Close", "volume": "Volume"})
bt_result = BtBacktest(df_bt, SmaCrossBt, cash=10_000, commission=0).run()
fx_result = Backtest(df_raw, SmaCrossFx, cash=10_000, commission=0, spread=0).run()
# ── compare ───────────────────────────────────────────────────────────────────
print("=" * 45)
print(f"{'Metric':<20} {'backtesting.py':>12} {'backtestingfx':>12}")
print("=" * 45)
print(f"{'Trades':<20} {bt_result['# Trades']:>12} {fx_result.num_trades:>12}")
print(f"{'Win Rate %':<20} {bt_result['Win Rate [%]']:>12.1f} {fx_result.win_rate_pct:>12.1f}")
print(f"{'Return %':<20} {bt_result['Return [%]']:>12.2f} {fx_result.total_return_pct:>12.2f}")
print(f"{'Max Drawdown %':<20} {bt_result['Max. Drawdown [%]']:>12.2f} {fx_result.max_drawdown_pct:>12.2f}")
print("=" * 45)
print()
print("Note: Return % differs because backtesting.py sizes by available cash,")
print(" backtestingfx uses fixed 0.1 lots. Trade count + win rate should match.")