Add broker preset backtest runner for XM and FBS

This commit is contained in:
chrisnov-it
2026-05-19 20:15:42 +08:00
parent 0c239418a6
commit 6185ce862f
4 changed files with 279 additions and 0 deletions
+21
View File
@@ -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.
+46
View File
@@ -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
}
}
]
}
+57
View File
@@ -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
}
}
]
}
+155
View File
@@ -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())