135 lines
4.7 KiB
Python
135 lines
4.7 KiB
Python
"""Parse the MT5 optimizer .set file into structured params + search space.
|
|
|
|
The MT5 optimizer .set format (one line per input):
|
|
|
|
Name=value||start||min||max||optimize(Y/N)
|
|
|
|
- ``value`` : the current/last-used value (the frozen baseline).
|
|
- ``start`` : the optimization start value (usually == value).
|
|
- ``min``/``max`` : the optimization range boundaries.
|
|
- ``optimize`` : ``Y`` = included in MT5's grid search, ``N`` = frozen.
|
|
|
|
This is the authoritative source for the search space (doc 05 §2) — the
|
|
broker's own declared ranges, not guesses. We mirror them exactly in the
|
|
Optuna ``SearchSpace`` so Python and MT5 explore the same parameter volume.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
@dataclass
|
|
class SetParam:
|
|
"""One input from the MT5 optimizer .set."""
|
|
|
|
name: str
|
|
value: Any # current/last-used value (frozen baseline if not optimized)
|
|
start: Any # optimization start (usually == value)
|
|
min_val: Any # optimization min
|
|
max_val: Any # optimization max
|
|
optimize: bool # Y = in MT5 grid search, N = frozen
|
|
raw_type: str = "" # inferred wire type ("int" / "float" / "bool" / "enum")
|
|
|
|
|
|
# Enum integer mappings (from the EA source, doc 05 §2).
|
|
# MT5 stores enums as integers; we keep them as ints and map back to names
|
|
# only for human-readable output.
|
|
ENUM_SIZING_MODE = {0: "SIZE_FIXED_LOT", 1: "SIZE_RISK_PERCENT"}
|
|
ENUM_STOP_MODE = {0: "STOP_ATR", 1: "STOP_POINTS"}
|
|
ENUM_TIMEFRAMES = {1: "PERIOD_M1", 5: "PERIOD_M5", 15: "PERIOD_M15",
|
|
30: "PERIOD_M30", 60: "PERIOD_H1", 240: "PERIOD_H4",
|
|
1440: "PERIOD_D1"}
|
|
|
|
|
|
def parse_set_file(path: str | Path) -> list[SetParam]:
|
|
"""Parse an MT5 optimizer ``.set`` (UTF-16-LE) into a list of SetParam.
|
|
|
|
Handles the MT5-native UTF-16-LE encoding. Lines starting with ``;`` are
|
|
comments / group headers. The trailing ``InpComment`` line has no
|
|
``||`` fields and is parsed as a plain string value.
|
|
"""
|
|
p = Path(path)
|
|
raw = p.read_bytes()
|
|
# Detect BOM / encoding.
|
|
if raw[:2] in (b"\xff\xfe", b"\xfe\xff"):
|
|
text = raw.decode("utf-16")
|
|
else:
|
|
text = raw.decode("utf-8", errors="replace")
|
|
|
|
params: list[SetParam] = []
|
|
for line in text.splitlines():
|
|
line = line.strip()
|
|
if not line or line.startswith(";") or "=" not in line:
|
|
continue
|
|
name, _, rest = line.partition("=")
|
|
name = name.strip()
|
|
fields = rest.split("||")
|
|
if len(fields) >= 5:
|
|
value = _cast(name, fields[0])
|
|
start = _cast(name, fields[1])
|
|
mn = _cast(name, fields[2])
|
|
mx = _cast(name, fields[3])
|
|
opt = fields[4].strip().upper() == "Y"
|
|
ptype = _infer_type(name, fields[0])
|
|
params.append(SetParam(name, value, start, mn, mx, opt, ptype))
|
|
else:
|
|
# Plain key=value (e.g. InpComment=GoldScalperPro).
|
|
value = _cast(name, fields[0])
|
|
params.append(SetParam(name, value, value, value, value, False,
|
|
_infer_type(name, fields[0])))
|
|
return params
|
|
|
|
|
|
def _cast(name: str, raw: str) -> Any:
|
|
"""Cast a raw string field to int/float/bool based on name + content."""
|
|
s = raw.strip()
|
|
if s.lower() in ("true", "false"):
|
|
return s.lower() == "true"
|
|
# Booleans as 0/1 for enum fields.
|
|
if name in ("InpUseBreakEven", "InpUseTrailing", "InpUseSession"):
|
|
# In the .set these appear as true/false strings, handled above.
|
|
return s
|
|
# Try int first (MT5 stores whole-number floats as ints sometimes).
|
|
try:
|
|
return int(s)
|
|
except ValueError:
|
|
pass
|
|
try:
|
|
return float(s)
|
|
except ValueError:
|
|
pass
|
|
return s
|
|
|
|
|
|
def _infer_type(name: str, raw: str) -> str:
|
|
"""Infer the wire type for set-file generation."""
|
|
s = raw.strip().lower()
|
|
if s in ("true", "false"):
|
|
return "bool"
|
|
if name in ("InpSizingMode", "InpStopMode", "InpTimeframe"):
|
|
return "enum"
|
|
try:
|
|
int(s)
|
|
return "int"
|
|
except ValueError:
|
|
try:
|
|
float(s)
|
|
return "float"
|
|
except ValueError:
|
|
return "string"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
set_path = sys.argv[1] if len(sys.argv) > 1 else (
|
|
r"C:\Users\Administrator\AppData\Roaming\MetaQuotes\Terminal"
|
|
r"\010E047102812FC0C18890992854220E\MQL5\Profiles\Tester\GoldScalperPro.set"
|
|
)
|
|
for p in parse_set_file(set_path):
|
|
flag = "OPT" if p.optimize else "frozen"
|
|
print(f" {p.name:24s} = {str(p.value):>10s} [{p.raw_type:6s}] "
|
|
f"range=[{p.min_val}..{p.max_val}] {flag}")
|