From dd90d425372241f874befd9238cf88409ff98f9f Mon Sep 17 00:00:00 2001 From: ZhijuCen Date: Wed, 24 Jun 2026 16:15:45 +0800 Subject: [PATCH] fix: correct backtesting limitations and simplify mql5_helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SKILL.md Section 6: - Add GUI-only declaration for Strategy Tester - Document metatester64.exe (remote agent mgmt only) and terminal64.exe (no CLI) - Add CLI Automation table: compile ✅, syntax check ✅, backtest ❌, optimization ❌ - Document MetaEditor CLI syntax: /compile:"path" /log and /log /s - Add Parameter Optimization section with [start, stop, step] format - Revise Backtesting Workflow with CLI step - Replace "Automated Backtesting Loop" with accurate "EA Development Cycle" mql5_helper.py: - Simplify detect_type: path-only detection (remove content-based reading) - Fix compile arg format: /compile:"relpath" /log (was wrong format) - Add check subcommand: syntax-only verification via /s flag - Extract _find_editor() shared by compile and check - Remove unused 'import os' - Simplify main() with dict dispatch --- skills/mql5/SKILL.md | 69 ++++++++--- skills/mql5/scripts/mql5_helper.py | 181 ++++++++++++++++++----------- 2 files changed, 170 insertions(+), 80 deletions(-) diff --git a/skills/mql5/SKILL.md b/skills/mql5/SKILL.md index 55e48c0..240da9f 100644 --- a/skills/mql5/SKILL.md +++ b/skills/mql5/SKILL.md @@ -482,6 +482,28 @@ The Strategy Tester is built into MT5. Key concepts: 2. **Optimization**: genetic algorithm searches parameter space 3. **Custom Criterion**: `OnTester()` returns optimization value +**Important**: The Strategy Tester is GUI-only. `metatester64.exe` only manages +remote testing agents (install/start/stop), not test execution itself. +`terminal64.exe` has no command-line parameters. Backtesting and optimization +must be performed through the MT5 Strategy Tester GUI. + +### CLI Automation — What Can and Cannot Be Automated + +| Task | CLI Possible? | How | +|------|:---:|-----| +| Syntax check | ✅ | `wine MetaEditor64.exe /compile:"path" /log /s` | +| Compile .mq5 → .ex5 | ✅ | `wine MetaEditor64.exe /compile:"path" /log` | +| Run backtest | ❌ | GUI only: Strategy Tester | +| Run optimization | ❌ | GUI only: Strategy Tester | +| Parse test report | ✅ | `scripts/parse_tester_report.py` | + +MetaEditor CLI syntax (Linux/Wine, from MT5 base directory): +``` +wine MetaEditor64.exe /compile:"MQL5/Experts/MyEA.mq5" /log # compile +wine MetaEditor64.exe /compile:"MQL5/Experts/MyEA.mq5" /log /s # syntax check only +``` +Log file: same directory as source, same name with `.log` extension. + ### OnTester Handler ```mql5 @@ -517,29 +539,48 @@ double OnTester() { | `STAT_EXPECTED_PAYOFF` | Average profit per trade | | `STAT_RECOVERY_FACTOR` | Profit / max drawdown | +### Parameter Optimization + +When running optimization in the GUI, define parameter ranges as +`[start, stop, step]` (stop inclusive). For example: + +| Parameter | Start | Stop | Step | +|-----------|-------|------|------| +| RiskPercent | 0.5 | 3.0 | 0.5 | +| Slippage | 5 | 20 | 5 | +| MagicNumber | 10000 | 10010 | 1 | + +In MT5 Strategy Tester: set each `input` parameter to "Enable optimization", +then configure range/step in the optimization tab. + ### Backtesting Workflow 1. Code the EA with `OnTick()`, `OnInit()`, `OnDeinit()` 2. Add `OnTester()` for custom optimization criterion -3. In MT5: Strategy Tester → select EA → set symbol/timeframe/period -4. Choose "Open prices only" for speed, "Every tick" for accuracy -5. Run single test → check results -6. Run optimization → find best parameters -7. Validate with out-of-sample data +3. **Compile and check syntax** via CLI (see CLI Automation above) +4. In MT5: Strategy Tester → select EA → set symbol/timeframe/period +5. Choose "Open prices only" for speed, "Every tick" for accuracy +6. Run single test → check results +7. Run optimization → find best parameters +8. Validate with out-of-sample data -### Automated Backtesting Loop +### EA Development Cycle ``` -EA Development Cycle: - Code → Compile → Single Test → Check Results - ↓ - If promising → Optimize → Analyze Results - ↓ - If validated → Forward Test → Deploy - ↓ - Monitor → Collect Data → Refine → Repeat +Code → Syntax Check (CLI) → Compile (CLI) + ↓ +GUI: Single Test → Check Results + ↓ +If promising → GUI: Optimize → Analyze Report + ↓ +If validated → GUI: Forward Test → Deploy + ↓ +Monitor → Collect Data → Refine → Repeat ``` +Note: steps marked (CLI) can be automated via `mql5_helper.py` or direct +Wine commands. GUI steps require human interaction. + ### Report Analysis — Interpreting Tester Results After each backtest, MT5 exports an HTML report. Use diff --git a/skills/mql5/scripts/mql5_helper.py b/skills/mql5/scripts/mql5_helper.py index 570f76c..1b386ce 100644 --- a/skills/mql5/scripts/mql5_helper.py +++ b/skills/mql5/scripts/mql5_helper.py @@ -1,16 +1,19 @@ #!/usr/bin/env python3 """ -MQL5 development helper: compile, deploy, and manage EA/Indicator files. +MQL5 development helper: compile, check, deploy, and list EA/Indicator files. + +CLI-automatable tasks only. Backtesting and optimization are GUI-only +(Strategy Tester). Usage: python skills/mql5/scripts/mql5_helper.py compile FILE.mq5 + python skills/mql5/scripts/mql5_helper.py check FILE.mq5 python skills/mql5/scripts/mql5_helper.py deploy FILE.mq5 python skills/mql5/scripts/mql5_helper.py status python skills/mql5/scripts/mql5_helper.py list """ import argparse -import os import shutil import subprocess import sys @@ -31,9 +34,8 @@ TYPE_DIRS = { def detect_type(file_path: Path) -> str: - """Detect program type from file path or content.""" - parts = file_path.parts - for p in parts: + """Detect program type from file path segments (path-only, no content read).""" + for p in file_path.parts: pl = p.lower() if pl in ("experts", "advisor", "advisors"): return "expert" @@ -45,83 +47,130 @@ def detect_type(file_path: Path) -> str: return "service" if pl in ("include",): return "include" - - # Detect from content - try: - content = file_path.read_text(errors="ignore")[:2000] - if "OnTick()" in content or "OnTester()" in content: - return "expert" - if "OnCalculate(" in content: - return "indicator" - if "OnStart()" in content: - return "script" - except Exception: - pass - return "expert" # default -def get_dest_dir(program_type: str) -> Path: - """Get destination directory for a program type.""" - subdir = TYPE_DIRS.get(program_type, "Experts") - return MQL5_DIR / subdir +def _find_editor() -> Path | None: + """Locate MetaEditor64.exe / MetaEditor.exe in MT5 installation.""" + candidates = [ + MT5_BASE / "MetaEditor64.exe", + MT5_BASE / "MetaEditor.exe", + ] + for p in MT5_BASE.rglob("MetaEditor*.exe"): + candidates.append(p) + for c in candidates: + if c.exists(): + return c + return None def cmd_compile(args): - """Compile an MQ5 file using MetaEditor via Wine.""" + """Compile an MQ5 file using MetaEditor via Wine. + + Deploys the file to the MQL5 directory first, then compiles. + Generates a .log file alongside the source. + """ src = Path(args.file).resolve() if not src.exists(): print(f"Error: {src} not found") return 1 - # Find metaeditor - candidates = [ - MT5_BASE / "metaeditor64.exe", - MT5_BASE / "metaeditor.exe", - ] - # Also check common alternative locations - for p in MT5_BASE.rglob("metaeditor*.exe"): - candidates.append(p) - - editor = None - for c in candidates: - if c.exists(): - editor = c - break - + editor = _find_editor() if not editor: - print("Error: metaeditor.exe not found in MT5 installation") + print("Error: MetaEditor.exe not found in MT5 installation") print(f"Searched: {MT5_BASE}") - print("Compilation must be done manually in MetaEditor IDE.") return 1 - # Deploy first, then compile + # Deploy to MQL5 tree program_type = detect_type(src) - dest_dir = get_dest_dir(program_type) + dest_dir = MQL5_DIR / TYPE_DIRS.get(program_type, "Experts") dest = dest_dir / src.name - dest_dir.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dest) print(f"Deployed: {src.name} → {dest}") - # Compile via Wine + # Compile: /compile:"path" /log + rel = dest.relative_to(MT5_BASE) try: result = subprocess.run( - ["wine", str(editor), "/compile", str(dest), "/include", str(MQL5_DIR / "Include")], - capture_output=True, text=True, timeout=30, + ["wine", str(editor), f'/compile:"{rel}"', "/log"], + capture_output=True, text=True, timeout=60, + cwd=str(MT5_BASE), ) print(f"Compile output:\n{result.stdout}") - if result.returncode != 0: - print(f"Compile errors:\n{result.stderr}") - return result.returncode + if result.stderr: + print(f"Stderr:\n{result.stderr}") + # Check for .log file + log_path = dest.with_suffix(".log") + if log_path.exists(): + log_text = log_path.read_text(errors="replace") + # Print last 30 lines of log for quick review + lines = log_text.strip().splitlines() + if lines: + print(f"\nCompilation log ({len(lines)} lines):") + for line in lines[-30:]: + print(f" {line}") + return result.returncode except FileNotFoundError: print("Error: wine not found. Install wine first.") return 1 except subprocess.TimeoutExpired: - print("Compile timed out (30s). Check MetaEditor for errors.") + print("Compile timed out (60s). Check MetaEditor for errors.") return 1 - return 0 + +def cmd_check(args): + """Syntax-check an MQ5 file without full compilation (/s flag). + + Deploys the file to the MQL5 directory first, then runs syntax check. + Generates a .log file alongside the source. + """ + src = Path(args.file).resolve() + if not src.exists(): + print(f"Error: {src} not found") + return 1 + + editor = _find_editor() + if not editor: + print("Error: MetaEditor.exe not found in MT5 installation") + print(f"Searched: {MT5_BASE}") + return 1 + + # Deploy to MQL5 tree + program_type = detect_type(src) + dest_dir = MQL5_DIR / TYPE_DIRS.get(program_type, "Experts") + dest = dest_dir / src.name + dest_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dest) + print(f"Deployed: {src.name} → {dest}") + + # Syntax check: /compile:"path" /log /s + rel = dest.relative_to(MT5_BASE) + try: + result = subprocess.run( + ["wine", str(editor), f'/compile:"{rel}"', "/log", "/s"], + capture_output=True, text=True, timeout=60, + cwd=str(MQL5_DIR.parent), + ) + print(f"Syntax check output:\n{result.stdout}") + if result.stderr: + print(f"Stderr:\n{result.stderr}") + # Check for .log file + log_path = dest.with_suffix(".log") + if log_path.exists(): + log_text = log_path.read_text(errors="replace") + lines = log_text.strip().splitlines() + if lines: + print(f"\nSyntax check log ({len(lines)} lines):") + for line in lines[-30:]: + print(f" {line}") + return result.returncode + except FileNotFoundError: + print("Error: wine not found. Install wine first.") + return 1 + except subprocess.TimeoutExpired: + print("Syntax check timed out (60s).") + return 1 def cmd_deploy(args): @@ -132,14 +181,13 @@ def cmd_deploy(args): return 1 program_type = detect_type(src) - dest_dir = get_dest_dir(program_type) + dest_dir = MQL5_DIR / TYPE_DIRS.get(program_type, "Experts") dest = dest_dir / src.name - dest_dir.mkdir(parents=True, exist_ok=True) shutil.copy2(src, dest) print(f"Deployed: {src.name} → {dest}") print(f"Type: {program_type}") - print(f"Compile in MetaEditor to generate .ex5") + print("Compile in MetaEditor or via: mql5_helper.py compile FILE.mq5") return 0 @@ -157,7 +205,6 @@ def cmd_status(args): ex5 = list(d.rglob("*.ex5")) print(f" {subdir:12s}: {len(mq5)} .mq5, {len(ex5)} .ex5") - # Check account info via Wine terminal info print(f"\nTerminal: {MT5_BASE / 'terminal64.exe'}") print(f" Exists: {(MT5_BASE / 'terminal64.exe').exists()}") return 0 @@ -183,7 +230,8 @@ def cmd_list(args): def main(): - parser = argparse.ArgumentParser(description="MQL5 development helper") + parser = argparse.ArgumentParser( + description="MQL5 development helper (compile, check, deploy)") sub = parser.add_subparsers(dest="command", required=True) sub.add_parser("status", help="Show MT5 installation status") @@ -192,21 +240,22 @@ def main(): compile_p = sub.add_parser("compile", help="Compile an MQ5 file") compile_p.add_argument("file", help="Path to .mq5 file") + check_p = sub.add_parser("check", help="Syntax-check an MQ5 file (no .ex5 output)") + check_p.add_argument("file", help="Path to .mq5 file") + deploy_p = sub.add_parser("deploy", help="Deploy MQ5 to MQL5 directory") deploy_p.add_argument("file", help="Path to .mq5 file") args = parser.parse_args() - if args.command == "status": - return cmd_status(args) - elif args.command == "list": - return cmd_list(args) - elif args.command == "compile": - return cmd_compile(args) - elif args.command == "deploy": - return cmd_deploy(args) - - return 0 + commands = { + "status": cmd_status, + "list": cmd_list, + "compile": cmd_compile, + "check": cmd_check, + "deploy": cmd_deploy, + } + return commands[args.command](args) if __name__ == "__main__":