"""tester.ini generator (doc 07 §2b). A small INI tells the tester *what* to run: expert, symbol, period, tick model, date range, deposit, leverage, report name, and ``ShutdownTerminal=1`` so the terminal closes itself when done (the bridge then knows it's finished). """ from __future__ import annotations import configparser from dataclasses import dataclass from pathlib import Path from typing import Optional DEFAULT_MT5_INSTALL = r"C:\Program Files\MetaTrader 5 IC Markets Global" # Tick models (doc 07 §2b): # 0 = Every tick based on real ticks (highest accuracy, slowest) # 1 = Every tick (generated from M1) # 2 = 1-minute OHLC (good for non-tick-sensitive, fast) — routine verification # 4 = Open prices only (rough, fastest) MODEL_OHLC = 2 MODEL_EVERY_TICK = 1 MODEL_REAL_TICKS = 0 MODEL_OPEN_PRICES = 4 @dataclass class TesterConfig: """Inputs for one tester run (doc 07 §2b). ``FromDate`` / ``ToDate`` are formatted ``YYYY.MM.DD`` for MT5. The login section lives separately (kept out of code via the env file, doc 07 §5). """ expert: str # e.g. "Experts\\MyEA.ex5" symbol: str period: str = "H1" # chart timeframe (≤ the EA's signal timeframe) model: int = MODEL_OHLC # tick model from_date: str = "2024.01.01" # YYYY.MM.DD to_date: str = "2026.01.01" deposit: float = 10000.0 leverage: int = 100 report: str = "report_myrun" # output HTML name (no extension) shutdown_terminal: bool = True set_file: Optional[str] = None # path to the .set, or None to inline login: Optional[int] = None # from env, never hard-coded password: Optional[str] = None # from env, never hard-coded server: Optional[str] = None # broker server def write_tester_ini(cfg: TesterConfig, path: str | Path) -> str: """Write a ``tester.ini`` for ``terminal64.exe /config:`` and return its path.""" p = Path(path) p.parent.mkdir(parents=True, exist_ok=True) cp = configparser.ConfigParser() cp.optionxform = str # preserve case (MT5 keys are case-sensitive) cp["Tester"] = { "Expert": cfg.expert, "Symbol": cfg.symbol, "Period": cfg.period, "Model": str(cfg.model), "FromDate": cfg.from_date, "ToDate": cfg.to_date, "Deposit": str(cfg.deposit), "Leverage": str(cfg.leverage), "Report": cfg.report, "ShutdownTerminal": "1" if cfg.shutdown_terminal else "0", } if cfg.set_file: cp["Tester"]["TestReplaceExpert"] = "0" common: dict[str, str] = {} if cfg.login is not None: common["Login"] = str(cfg.login) if cfg.password is not None: common["Password"] = cfg.password if cfg.server is not None: common["Server"] = cfg.server if common: cp["Common"] = common with p.open("w", encoding="utf-8") as f: cp.write(f) return str(p)