7a3e13a734
Full optimization system for LEGSTECH_EA_V2: - Flask + SocketIO live dashboard (dark premium UI) - MT5 process control (auto-kill, clean launch, retry) - HTML report parser (UTF-16 LE, 597 trades, metrics) - Pre-run validation and actionable error messages - Analysis engines: Reversal, TimePerfomance, EntryExit, EquityCurve - Composite scoring (Calmar-primary) - Mutation engine with knowledge_base.yaml - Validation gate: IS + Walk-Forward - Reports folder with HTML/CSV per run - Double-click launcher batch file
164 lines
5.7 KiB
Python
164 lines
5.7 KiB
Python
"""
|
|
mt5/ini_builder.py
|
|
Generates the MT5 strategy tester .ini file from a parameter dict + config.
|
|
"""
|
|
from __future__ import annotations
|
|
import configparser
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import yaml
|
|
from loguru import logger
|
|
|
|
|
|
# MT5 period string names (used in [Tester] Period= field)
|
|
TIMEFRAME_NAMES = {
|
|
"M1": "M1", "M5": "M5", "M15": "M15", "M30": "M30",
|
|
"H1": "H1", "H4": "H4", "D1": "D1", "W1": "W1", "MN": "MN1",
|
|
}
|
|
|
|
# MT5 tester model codes
|
|
MODEL_CODES = {
|
|
"every_tick": 0,
|
|
"every_tick_real": 1,
|
|
"ohlc_m1": 4,
|
|
}
|
|
|
|
|
|
class IniBuilder:
|
|
"""
|
|
Builds MT5 strategy tester .ini files.
|
|
|
|
Usage:
|
|
builder = IniBuilder(config_path="config.yaml", manifest_path="mutation/param_manifest.yaml")
|
|
ini_path = builder.build(
|
|
run_id="run_001",
|
|
params={"InpRiskPercent": 1.5, "InpUseTrailing": True, ...},
|
|
period_start="2022.01.01",
|
|
period_end="2023.12.31",
|
|
output_dir=Path("runs/run_001"),
|
|
)
|
|
"""
|
|
|
|
def __init__(self, config_path: str | Path, manifest_path: str | Path):
|
|
with open(config_path) as f:
|
|
self.cfg = yaml.safe_load(f)
|
|
with open(manifest_path) as f:
|
|
self.manifest = yaml.safe_load(f)["parameters"]
|
|
|
|
# ── Public ────────────────────────────────────────────────────────────────
|
|
|
|
def build(
|
|
self,
|
|
run_id: str,
|
|
params: dict[str, Any],
|
|
period_start: str,
|
|
period_end: str,
|
|
output_dir: Path,
|
|
phase: str = "explore",
|
|
) -> Path:
|
|
"""
|
|
Write <run_id>.ini to output_dir and return its path.
|
|
params: dict of EA input values (partial OK — missing params use manifest defaults)
|
|
"""
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
ini_path = output_dir / f"{run_id}.ini"
|
|
report_dir = output_dir / "report"
|
|
report_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
full_params = self._merge_with_defaults(params)
|
|
ini_text = self._render(run_id, full_params, period_start, period_end,
|
|
report_dir, phase)
|
|
|
|
ini_path.write_text(ini_text, encoding="utf-8")
|
|
logger.debug(f"INI written: {ini_path}")
|
|
return ini_path
|
|
|
|
def default_params(self) -> dict[str, Any]:
|
|
"""Return all EA parameters at their default values."""
|
|
return self._merge_with_defaults({})
|
|
|
|
# ── Internal ──────────────────────────────────────────────────────────────
|
|
|
|
def _merge_with_defaults(self, overrides: dict[str, Any]) -> dict[str, Any]:
|
|
"""Merge caller-supplied overrides with manifest defaults."""
|
|
result: dict[str, Any] = {}
|
|
for name, spec in self.manifest.items():
|
|
if name in overrides:
|
|
result[name] = overrides[name]
|
|
else:
|
|
result[name] = spec.get("default", 0)
|
|
return result
|
|
|
|
def _format_value(self, name: str, value: Any) -> str:
|
|
"""Format a parameter value for the [TesterInputs] section."""
|
|
spec = self.manifest.get(name, {})
|
|
ptype = spec.get("type", "float")
|
|
|
|
if ptype == "bool":
|
|
return "true" if value else "false"
|
|
if ptype == "fixed":
|
|
# Fixed params: write exact default
|
|
return str(value)
|
|
if ptype == "int" or ptype == "enum":
|
|
return str(int(value))
|
|
if ptype == "float":
|
|
# Determine decimal places from step
|
|
step = spec.get("step", 0.1)
|
|
decimals = len(str(step).split(".")[-1]) if "." in str(step) else 0
|
|
return f"{float(value):.{decimals}f}"
|
|
return str(value)
|
|
|
|
def _render(
|
|
self,
|
|
run_id: str,
|
|
params: dict[str, Any],
|
|
period_start: str,
|
|
period_end: str,
|
|
report_dir: Path,
|
|
phase: str,
|
|
) -> str:
|
|
"""Render final INI content as a string."""
|
|
ea_cfg = self.cfg["ea"]
|
|
mt5_cfg = self.cfg["mt5"]
|
|
broker_cfg = self.cfg["broker"]
|
|
|
|
# Period must be the string name (H1, M30 etc) — NOT the ENUM integer
|
|
tf_name = TIMEFRAME_NAMES.get(ea_cfg["timeframe"].upper(), "H1")
|
|
|
|
# Model: 0=Every Tick (slow), 4=OHLC M1 (fast, reliable for ini-based launch)
|
|
model_code = mt5_cfg.get("tester_model", 4)
|
|
|
|
# Report path must be RELATIVE to the MT5 terminal data folder
|
|
# MT5 appends its own base path. Use run_id as the report name.
|
|
report_name = f"Optimizer_{run_id}"
|
|
|
|
lines = [
|
|
f"; MT5 Optimizer INI — run_id={run_id} phase={phase}",
|
|
f"",
|
|
f"[Tester]",
|
|
f"Expert={ea_cfg['file']}",
|
|
f"Symbol={ea_cfg['symbol']}",
|
|
f"Period={tf_name}",
|
|
f"Optimization=0",
|
|
f"Model={model_code}",
|
|
f"FromDate={period_start}",
|
|
f"ToDate={period_end}",
|
|
f"ForwardMode=0",
|
|
f"Report={report_name}",
|
|
f"ReplaceReport=1",
|
|
f"ShutdownTerminal={mt5_cfg.get('shutdown_terminal', 1)}",
|
|
f"Deposit={broker_cfg['deposit']}",
|
|
f"Currency={broker_cfg['currency']}",
|
|
f"Leverage={broker_cfg['leverage']}",
|
|
f"",
|
|
f"[TesterInputs]",
|
|
]
|
|
|
|
for name, value in params.items():
|
|
formatted = self._format_value(name, value)
|
|
lines.append(f"{name}={formatted}")
|
|
|
|
lines.append("") # trailing newline
|
|
return "\n".join(lines)
|