Initial commit: multi-symbol bot with backtest engine and RSI trend strategy
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+224
@@ -0,0 +1,224 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user