mirror of
https://github.com/chrisnov-it/quantumbotx.git
synced 2026-07-27 18:57:47 +00:00
Add broker preset backtest runner for XM and FBS
This commit is contained in:
@@ -141,6 +141,27 @@ strategy testing. Typical workflows:
|
||||
- Run `python lab/download_data.py`.
|
||||
- Upload CSV files through the backtesting UI.
|
||||
|
||||
### Broker Preset Backtests (XM/FBS)
|
||||
|
||||
You can run a preset batch backtest tuned for broker environments:
|
||||
|
||||
```bash
|
||||
py -3.12 lab/run_preset_backtests.py
|
||||
```
|
||||
|
||||
Optional:
|
||||
|
||||
```bash
|
||||
py -3.12 lab/run_preset_backtests.py --preset xm_demo.json --tail 5000
|
||||
```
|
||||
|
||||
Preset files are stored in:
|
||||
|
||||
- `config/presets/xm_demo.json`
|
||||
- `config/presets/fbs_demo.json`
|
||||
|
||||
Results are saved to `logs/preset_backtest_results.json`.
|
||||
|
||||
Supported markets depend on the connected MT5 broker. Common examples include:
|
||||
|
||||
- Forex pairs such as EURUSD, GBPUSD, USDJPY.
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"preset_name": "FBS Demo Baseline",
|
||||
"broker_match": ["FBS"],
|
||||
"symbols": {
|
||||
"XAUUSD": "XAUUSDw",
|
||||
"EURUSD": "EURUSDw",
|
||||
"GBPUSD": "GBPUSDw",
|
||||
"USDJPY": "USDJPYw",
|
||||
"BTCUSD": "BTCUSD"
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"name": "EURUSDw Conservative Trend",
|
||||
"symbol": "EURUSD",
|
||||
"data_file": "backtest_data/EURUSD_H1_data.csv",
|
||||
"strategy_id": "MA_CROSSOVER",
|
||||
"params": {
|
||||
"risk_percent": 0.5,
|
||||
"sl_atr_multiplier": 2.0,
|
||||
"tp_atr_multiplier": 4.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GBPUSDw Mean Reversion",
|
||||
"symbol": "GBPUSD",
|
||||
"data_file": "backtest_data/GBPUSD_H1_data.csv",
|
||||
"strategy_id": "BOLLINGER_REVERSION",
|
||||
"params": {
|
||||
"risk_percent": 0.5,
|
||||
"sl_atr_multiplier": 2.0,
|
||||
"tp_atr_multiplier": 4.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "XAUUSDw Defensive",
|
||||
"symbol": "XAUUSD",
|
||||
"data_file": "backtest_data/XAUUSD_H1_data.csv",
|
||||
"strategy_id": "TURTLE_BREAKOUT",
|
||||
"params": {
|
||||
"risk_percent": 0.25,
|
||||
"sl_atr_multiplier": 1.0,
|
||||
"tp_atr_multiplier": 2.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"preset_name": "XM Demo Baseline",
|
||||
"broker_match": ["XM", "XMGLOBAL"],
|
||||
"symbols": {
|
||||
"XAUUSD": "GOLD",
|
||||
"EURUSD": "EURUSD",
|
||||
"GBPUSD": "GBPUSD",
|
||||
"USDJPY": "USDJPY",
|
||||
"BTCUSD": "BTCUSD"
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"name": "EURUSD Conservative Trend",
|
||||
"symbol": "EURUSD",
|
||||
"data_file": "backtest_data/EURUSD_H1_data.csv",
|
||||
"strategy_id": "MA_CROSSOVER",
|
||||
"params": {
|
||||
"risk_percent": 0.5,
|
||||
"sl_atr_multiplier": 2.0,
|
||||
"tp_atr_multiplier": 4.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EURUSD Momentum",
|
||||
"symbol": "EURUSD",
|
||||
"data_file": "backtest_data/EURUSD_H1_data.csv",
|
||||
"strategy_id": "RSI_CROSSOVER",
|
||||
"params": {
|
||||
"risk_percent": 0.5,
|
||||
"sl_atr_multiplier": 2.0,
|
||||
"tp_atr_multiplier": 4.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "GOLD Defensive",
|
||||
"symbol": "XAUUSD",
|
||||
"data_file": "backtest_data/GOLD_H1_data.csv",
|
||||
"strategy_id": "MA_CROSSOVER",
|
||||
"params": {
|
||||
"risk_percent": 0.25,
|
||||
"sl_atr_multiplier": 1.0,
|
||||
"tp_atr_multiplier": 2.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "BTCUSD Volatility",
|
||||
"symbol": "BTCUSD",
|
||||
"data_file": "backtest_data/BTCUSD_H1_data.csv",
|
||||
"strategy_id": "BOLLINGER_SQUEEZE",
|
||||
"params": {
|
||||
"risk_percent": 0.5,
|
||||
"sl_atr_multiplier": 2.0,
|
||||
"tp_atr_multiplier": 4.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
from dotenv import load_dotenv
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
from core.backtesting.enhanced_engine import run_enhanced_backtest
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
PRESET_DIR = PROJECT_ROOT / "config" / "presets"
|
||||
|
||||
|
||||
def load_mt5_credentials() -> tuple[int, str, str]:
|
||||
load_dotenv(PROJECT_ROOT / ".env")
|
||||
login = int(os.getenv("MT5_LOGIN", "0"))
|
||||
password = os.getenv("MT5_PASSWORD", "")
|
||||
server = os.getenv("MT5_SERVER", "")
|
||||
if not login or not password or not server:
|
||||
raise RuntimeError("MT5 credentials missing in .env")
|
||||
return login, password, server
|
||||
|
||||
|
||||
def detect_broker_server(login: int, password: str, server: str) -> str:
|
||||
if not mt5.initialize(login=login, password=password, server=server): # type: ignore
|
||||
raise RuntimeError(f"MT5 init failed: {mt5.last_error()}") # type: ignore
|
||||
info = mt5.account_info() # type: ignore
|
||||
if not info:
|
||||
mt5.shutdown() # type: ignore
|
||||
raise RuntimeError("MT5 account_info failed")
|
||||
return str(info.server).upper()
|
||||
|
||||
|
||||
def choose_preset(broker_server: str, forced_preset: str | None) -> Path:
|
||||
if forced_preset:
|
||||
path = PRESET_DIR / forced_preset
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Preset not found: {path}")
|
||||
return path
|
||||
|
||||
for preset_path in PRESET_DIR.glob("*.json"):
|
||||
with open(preset_path, "r", encoding="utf-8") as f:
|
||||
preset = json.load(f)
|
||||
for token in preset.get("broker_match", []):
|
||||
if token.upper() in broker_server:
|
||||
return preset_path
|
||||
|
||||
return PRESET_DIR / "xm_demo.json"
|
||||
|
||||
|
||||
def run_case(case: dict, symbol_map: dict, tail_rows: int) -> dict:
|
||||
raw_symbol = case["symbol"]
|
||||
resolved_symbol = symbol_map.get(raw_symbol, raw_symbol)
|
||||
data_file = PROJECT_ROOT / case["data_file"]
|
||||
if not data_file.exists():
|
||||
return {
|
||||
"name": case["name"],
|
||||
"symbol": resolved_symbol,
|
||||
"strategy": case["strategy_id"],
|
||||
"status": "missing_data",
|
||||
"error": f"Missing file: {data_file}",
|
||||
}
|
||||
|
||||
df = pd.read_csv(data_file, parse_dates=["time"]).tail(tail_rows)
|
||||
result = run_enhanced_backtest(
|
||||
case["strategy_id"],
|
||||
case["params"],
|
||||
df,
|
||||
symbol_name=resolved_symbol,
|
||||
engine_config={
|
||||
"enable_spread_costs": True,
|
||||
"enable_slippage": True,
|
||||
"enable_realistic_execution": True,
|
||||
},
|
||||
)
|
||||
if "error" in result:
|
||||
return {
|
||||
"name": case["name"],
|
||||
"symbol": resolved_symbol,
|
||||
"strategy": case["strategy_id"],
|
||||
"status": "error",
|
||||
"error": result["error"],
|
||||
}
|
||||
|
||||
return {
|
||||
"name": case["name"],
|
||||
"symbol": resolved_symbol,
|
||||
"strategy": case["strategy_id"],
|
||||
"status": "ok",
|
||||
"gross_profit_usd": result.get("gross_profit_usd", 0),
|
||||
"net_profit_usd": result.get("net_profit_after_costs", 0),
|
||||
"spread_costs_usd": result.get("total_spread_costs", 0),
|
||||
"win_rate_percent": result.get("win_rate_percent", 0),
|
||||
"max_drawdown_percent": result.get("max_drawdown_percent", 0),
|
||||
"total_trades": result.get("total_trades", 0),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Run broker preset backtests")
|
||||
parser.add_argument("--preset", help="Preset file name in config/presets, e.g. xm_demo.json")
|
||||
parser.add_argument("--tail", type=int, default=5000, help="Number of latest rows to test per case")
|
||||
args = parser.parse_args()
|
||||
|
||||
login, password, server = load_mt5_credentials()
|
||||
broker_server = detect_broker_server(login, password, server)
|
||||
preset_path = choose_preset(broker_server, args.preset)
|
||||
|
||||
with open(preset_path, "r", encoding="utf-8") as f:
|
||||
preset = json.load(f)
|
||||
|
||||
print(f"Using broker server: {broker_server}")
|
||||
print(f"Using preset: {preset_path.name} ({preset.get('preset_name', 'Unnamed')})")
|
||||
print("")
|
||||
|
||||
rows = []
|
||||
for case in preset.get("cases", []):
|
||||
rows.append(run_case(case, preset.get("symbols", {}), args.tail))
|
||||
|
||||
mt5.shutdown() # type: ignore
|
||||
|
||||
print("name | symbol | strategy | status | net | gross | spread | win_rate | drawdown | trades")
|
||||
for r in rows:
|
||||
if r["status"] != "ok":
|
||||
print(f"{r['name']} | {r['symbol']} | {r['strategy']} | {r['status']} | {r.get('error', '')}")
|
||||
continue
|
||||
print(
|
||||
f"{r['name']} | {r['symbol']} | {r['strategy']} | ok | "
|
||||
f"{r['net_profit_usd']:.2f} | {r['gross_profit_usd']:.2f} | {r['spread_costs_usd']:.2f} | "
|
||||
f"{r['win_rate_percent']:.2f}% | {r['max_drawdown_percent']:.2f}% | {r['total_trades']}"
|
||||
)
|
||||
|
||||
out_dir = PROJECT_ROOT / "logs"
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
out_file = out_dir / "preset_backtest_results.json"
|
||||
with open(out_file, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{
|
||||
"broker_server": broker_server,
|
||||
"preset_file": preset_path.name,
|
||||
"results": rows,
|
||||
},
|
||||
f,
|
||||
indent=2,
|
||||
)
|
||||
print(f"\nSaved: {out_file}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user