#!/usr/bin/env python3 """ Main backtest runner. Steps ----- 1. Load EURJPY M15 data (2 years). 2. Run baseline RSI Mean Reversion (original strategy, no trend filter). 3. Run improved RSI Trend strategy with default params. 4. Grid-search optimise on in-sample (first 70%). 5. Walk-forward: run best params on out-of-sample (last 30%). 6. Print full reports and save equity-curve charts. """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) import numpy as np from engine.backtest import BacktestEngine from engine.data import load_candles from engine.metrics import compute, Metrics from engine.optimizer import grid_search from strategies.rsi_trend import RSITrendStrategy try: from reports.chart import save_equity_chart CHARTS = True except ImportError: CHARTS = False # ── config ──────────────────────────────────────────────────────────────────── SYMBOL = "EURJPY" TIMEFRAME = "M15" INITIAL_BALANCE = 10_000.0 RISK_PER_TRADE = 0.01 SPREAD_PIPS = 0.5 SPLIT_RATIO = 0.70 # in-sample fraction # ── parameter grid for optimisation ────────────────────────────────────────── PARAM_GRID = { "os_level": [25, 30, 35], "ob_level": [65, 70, 75], "sl_atr": [1.0, 1.5, 2.0], "tp_atr": [2.0, 3.0, 4.0], "trend_fast": [50], "trend_slow": [200], "session_start": [7], "session_end": [20], } # ── helpers ─────────────────────────────────────────────────────────────────── def divider(title: str = "") -> None: if title: pad = (60 - len(title) - 2) // 2 print(f"\n{'─'*pad} {title} {'─'*pad}") else: print("─" * 62) def print_report(label: str, metrics: Metrics, period: str = "") -> None: divider(label) if period: print(f" Period : {period}") print(metrics) # ── main ────────────────────────────────────────────────────────────────────── def main() -> None: print("=" * 62) print(" NewBot — RSI Trend Pullback | EURJPY M15") print("=" * 62) # 1. Load data print(f"\nLoading {SYMBOL} {TIMEFRAME}…") df = load_candles(SYMBOL, TIMEFRAME) print(f" {len(df):,} candles ({df.index[0].date()} → {df.index[-1].date()})") split_idx = int(len(df) * SPLIT_RATIO) df_in = df.iloc[:split_idx] df_out = df.iloc[split_idx:] print(f" In-sample : {df_in.index[0].date()} → {df_in.index[-1].date()} ({len(df_in):,} bars)") print(f" Out-sample : {df_out.index[0].date()} → {df_out.index[-1].date()} ({len(df_out):,} bars)") engine = BacktestEngine( SYMBOL, initial_balance = INITIAL_BALANCE, risk_per_trade = RISK_PER_TRADE, spread_pips = SPREAD_PIPS, ) # 2. Baseline: original RSI mean-reversion (no trend filter) # Simulated by using trend_fast=1, trend_slow=1 so trend is always 0 — # that short-circuits the trend guard. Instead we reuse the same class # but force trend filter OFF by setting trend EMAs equal (trend ≈ 0 always). # Simpler: use a tiny fast period so the two EMAs are always close → trend~0 # Actually the cleanest baseline: subclass and override signal to ignore trend. class BaselineRSI(RSITrendStrategy): """RSI mean reversion without any trend filter — take both longs & shorts.""" def signal(self, df, i): if i < 1: return None row = df.iloc[i] prev = df.iloc[i - 1] if any(df.iloc[i][c] != df.iloc[i][c] for c in ("rsi", "atr")): return None hour = row.name.hour if not (self.session_start <= hour < self.session_end): return None rsi_now, rsi_prev = row["rsi"], prev["rsi"] if __import__("pandas").isna(rsi_prev): return None atr = row["atr"] ref = row["close"] if rsi_prev < self.os_level and rsi_now >= self.os_level: return "long", ref - self.sl_atr * atr, ref + self.tp_atr * atr if rsi_prev > self.ob_level and rsi_now <= self.ob_level: return "short", ref + self.sl_atr * atr, ref - self.tp_atr * atr return None baseline = BaselineRSI(os_level=30, ob_level=70, sl_atr=1.5, tp_atr=3.0) bl_trades, bl_equity = engine.run(df, baseline) bl_metrics = compute(bl_trades, bl_equity, INITIAL_BALANCE) print_report("Baseline RSI (no trend filter) — full dataset", bl_metrics, f"{df.index[0].date()} → {df.index[-1].date()}") # 3. Improved strategy — default params, full dataset improved = RSITrendStrategy(os_level=30, ob_level=70, sl_atr=1.5, tp_atr=3.0) im_trades, im_equity = engine.run(df, improved) im_metrics = compute(im_trades, im_equity, INITIAL_BALANCE) print_report("Improved RSI Trend (default params) — full dataset", im_metrics, f"{df.index[0].date()} → {df.index[-1].date()}") # 4. Optimise on in-sample divider("Grid Search (in-sample optimisation)") n_combos = 1 for v in PARAM_GRID.values(): n_combos *= len(v) print(f" Testing {n_combos} parameter combinations on in-sample data…") results = grid_search( RSITrendStrategy, PARAM_GRID, df_in, engine, verbose=True ) best = results[0] print(f"\n Best score : {best.score:.4f}") print(f" Params : {best.params}") print(f"\n In-sample metrics:") print(best.metrics) print("\n Top-5 combinations:") print(f" {'Score':>8} {'WR%':>5} {'PF':>5} {'Sharpe':>6} {'Trades':>6} Params") for r in results[:5]: print(f" {r.score:8.4f} {r.metrics.win_rate*100:5.1f} " f"{r.metrics.profit_factor:5.2f} {r.metrics.sharpe:6.2f} " f"{r.metrics.n_trades:6d} {r.params}") # 5. Walk-forward: run best params on out-of-sample opt_strategy = RSITrendStrategy(**best.params) oos_trades, oos_equity = engine.run(df_out, opt_strategy) oos_metrics = compute(oos_trades, oos_equity, INITIAL_BALANCE) print_report("Walk-Forward — out-of-sample (best params)", oos_metrics, f"{df_out.index[0].date()} → {df_out.index[-1].date()}") # 6. Also run best params on full dataset for the final picture full_strategy = RSITrendStrategy(**best.params) full_trades, full_equity = engine.run(df, full_strategy) full_metrics = compute(full_trades, full_equity, INITIAL_BALANCE) print_report("Optimised Strategy — full dataset", full_metrics, f"{df.index[0].date()} → {df.index[-1].date()}") # 7. Summary comparison table divider("Strategy Comparison Summary") print(f" {'Strategy':<35} {'Trades':>6} {'WR%':>5} {'PF':>5} {'Sharpe':>6} {'Return%':>7} {'MDD%':>6}") rows = [ ("Baseline RSI (no filter)", bl_metrics), ("RSI Trend (default params)", im_metrics), (f"RSI Trend (optimised, in-sample)", best.metrics), (f"RSI Trend (optimised, out-sample)", oos_metrics), (f"RSI Trend (optimised, full data)", full_metrics), ] for name, m in rows: print(f" {name:<35} {m.n_trades:>6} {m.win_rate*100:>5.1f} " f"{m.profit_factor:>5.2f} {m.sharpe:>6.2f} " f"{m.total_return*100:>+7.2f} {m.max_drawdown*100:>6.2f}") # 8. Charts if CHARTS: divider("Saving charts") charts_dir = Path(__file__).parent / "reports" charts_dir.mkdir(exist_ok=True) paths = [] pairs = [ (bl_equity, bl_trades, bl_metrics, "Baseline RSI (no filter)", "baseline.png"), (im_equity, im_trades, im_metrics, "RSI Trend — default params", "improved_default.png"), (full_equity, full_trades, full_metrics, "RSI Trend — optimised (full data)","optimised_full.png"), (oos_equity, oos_trades, oos_metrics, "RSI Trend — out-of-sample (WF)", "walkforward_oos.png"), ] for eq, tr, me, lbl, fname in pairs: p = save_equity_chart(eq, tr, me, lbl, fname) paths.append(p) print(f" Saved: {p}") else: print("\n (matplotlib not available — skipping charts)") print("\n" + "=" * 62) print(" Done.") print("=" * 62) if __name__ == "__main__": main()