fix: V4-style EAs now expose optimizable params (auto-infer ranges)
Bug: a .set file saved out of MT5's "save settings" button (rather than "save optimization") contains bare \`Inp...=value\` lines with no \`|min|max|step\` metadata. The parser previously marked every such param as type="fixed", and /api/ea_params filters fixed params out, so the user saw "No optimizable parameters found in .set file" and could not start a run with that EA. Reproduces with LEGSTECH_EA_V4 in this user's setup. Fix: - SetParser.parse() now takes an auto_infer_ranges flag (default True). When a line has no '|' separator, the parser falls back to a heuristic range based on the param's name + value: * `*Pips`, `*Period`, `*Lookback`, `*Spread`, `*Trades` → integer ±50% / ±150% * `*Percent`, `*Pct`, `*Risk`, `*Buffer`, `*Multiplier`, `*Ratio` → float ±50% / ×2 * `RRRatio` → 0.5–2.5x with 0.25 step * `Score` → 0.5–1.5x with 0.05 step * `Lot*` → ±50% with 0.01 step * `Hour*` / `Session*` → 0–23 * Pure 0/1 with `Use*`/`Enable*`/`Allow*` prefix → bool * `true`/`false` literals → bool * Generic numeric fallback → ±50% with type-appropriate step - Added timeframe enum names (htf/mtf/ltf/_tf/timeframe) and debug/log flags to _FORCE_FIXED_PATTERNS so MT5 internal constants like InpHTF=16388 don't get incorrectly inferred as scalars. Verified on LEGSTECH_EA_V4 (raw saved-settings format): 64/71 params now optimizable (was 0); regression-checked LEGSTECH_EA_V2 still returns 42 params from its existing optimization metadata.
This commit is contained in:
@@ -38,9 +38,15 @@ _TESTER_KEYS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# EA params that should always be fixed even if they have a range
|
# EA params that should always be fixed even if they have a range
|
||||||
|
# Includes magic numbers, debug/UI flags, and MT5 timeframe enum constants
|
||||||
|
# (HTF/MTF/LTF values like 16388 are PERIOD_H1/etc — not optimizable scalars).
|
||||||
_FORCE_FIXED_PATTERNS = [
|
_FORCE_FIXED_PATTERNS = [
|
||||||
"testermode", "testeri", "testerinit", "showpanel",
|
"testermode", "testeri", "testerinit", "showpanel",
|
||||||
"magicnumber", "magic",
|
"magicnumber", "magic",
|
||||||
|
"htf", "mtf", "ltf", # higher/medium/lower timeframe enums
|
||||||
|
"_tf", "timeframe",
|
||||||
|
"comment", "label", "prefix",
|
||||||
|
"verbose", "debug", "log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -64,6 +70,7 @@ class SetParser:
|
|||||||
default_optimize: bool = False,
|
default_optimize: bool = False,
|
||||||
force_optimize: Optional[set[str]] = None,
|
force_optimize: Optional[set[str]] = None,
|
||||||
force_fixed: Optional[set[str]] = None,
|
force_fixed: Optional[set[str]] = None,
|
||||||
|
auto_infer_ranges: bool = True,
|
||||||
) -> ParameterSchema:
|
) -> ParameterSchema:
|
||||||
"""
|
"""
|
||||||
Parse a .set file and return a ParameterSchema.
|
Parse a .set file and return a ParameterSchema.
|
||||||
@@ -119,6 +126,14 @@ class SetParser:
|
|||||||
logger.debug(f"SetParser: skipped unrecognised line: {line!r}")
|
logger.debug(f"SetParser: skipped unrecognised line: {line!r}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# If the file had no optimization metadata (just `Name=value`),
|
||||||
|
# heuristically infer a reasonable range so the user can still
|
||||||
|
# optimize. Triggered for raw MT5 "saved settings" .set files.
|
||||||
|
if auto_infer_ranges and param.type == "fixed" and "|" not in rest:
|
||||||
|
inferred = self._infer_range_from_value(name, rest)
|
||||||
|
if inferred is not None:
|
||||||
|
param = inferred
|
||||||
|
|
||||||
# Apply force-fixed overrides
|
# Apply force-fixed overrides
|
||||||
if name in force_fixed or self._is_force_fixed(name):
|
if name in force_fixed or self._is_force_fixed(name):
|
||||||
param.type = "fixed"
|
param.type = "fixed"
|
||||||
@@ -276,3 +291,116 @@ class SetParser:
|
|||||||
"""Return True for params that are always fixed regardless of their range."""
|
"""Return True for params that are always fixed regardless of their range."""
|
||||||
lower = name.lower()
|
lower = name.lower()
|
||||||
return any(pat in lower for pat in _FORCE_FIXED_PATTERNS)
|
return any(pat in lower for pat in _FORCE_FIXED_PATTERNS)
|
||||||
|
|
||||||
|
# ── Range inference for "saved settings" .set files ──────────────────────
|
||||||
|
|
||||||
|
def _infer_range_from_value(self, name: str, raw: str) -> Optional[ParameterDef]:
|
||||||
|
"""
|
||||||
|
Heuristic range inference for .set files that contain bare `Name=value`
|
||||||
|
lines (no `|min|max|step` metadata). Common with files saved out of MT5
|
||||||
|
directly rather than exported as an optimization preset.
|
||||||
|
|
||||||
|
Returns None to leave the param as fixed (e.g. magic numbers, strings).
|
||||||
|
"""
|
||||||
|
lower = name.lower()
|
||||||
|
|
||||||
|
# Force-fixed by name pattern → keep fixed
|
||||||
|
if self._is_force_fixed(name):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Bool: explicit true/false, or value 0/1 + name implies a toggle
|
||||||
|
if raw.lower() in ("true", "false"):
|
||||||
|
default_b = raw.lower() == "true"
|
||||||
|
return ParameterDef(
|
||||||
|
name=name, type="bool", default=default_b,
|
||||||
|
min=0.0, max=1.0, step=1.0, optimize=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
v = float(raw)
|
||||||
|
except ValueError:
|
||||||
|
return None # non-numeric (e.g. enum string) → leave fixed
|
||||||
|
|
||||||
|
is_int_value = ("." not in raw) and v == int(v)
|
||||||
|
toggle_names = ("use", "enable", "allow", "show", "is_", "include", "with")
|
||||||
|
looks_toggle = any(lower.startswith(p) for p in toggle_names) and v in (0, 1)
|
||||||
|
if looks_toggle:
|
||||||
|
return ParameterDef(
|
||||||
|
name=name, type="bool", default=bool(int(v)),
|
||||||
|
min=0.0, max=1.0, step=1.0, optimize=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if v == 0:
|
||||||
|
# Can't ±% a zero meaningfully — give a small fixed nudge
|
||||||
|
if is_int_value:
|
||||||
|
return ParameterDef(name=name, type="int", default=0,
|
||||||
|
min=0.0, max=10.0, step=1.0, optimize=False)
|
||||||
|
return ParameterDef(name=name, type="float", default=0.0,
|
||||||
|
min=0.0, max=1.0, step=0.05, optimize=False)
|
||||||
|
|
||||||
|
# Pattern-based ranges by name suffix / keyword
|
||||||
|
# (lower-bound, upper-bound multipliers, step, force_int)
|
||||||
|
patterns = [
|
||||||
|
("pips", 0.5, 2.0, None, True),
|
||||||
|
("period", 0.5, 2.0, 1.0, True),
|
||||||
|
("lookback", 0.5, 2.0, 1.0, True),
|
||||||
|
("multiplier", 0.5, 2.0, None, False),
|
||||||
|
("ratio", 0.5, 2.0, None, False),
|
||||||
|
("rrratio", 0.5, 2.5, 0.25, False),
|
||||||
|
("percent", 0.5, 2.0, None, False),
|
||||||
|
("pct", 0.5, 2.0, None, False),
|
||||||
|
("risk", 0.5, 2.0, None, False),
|
||||||
|
("buffer", 0.5, 2.0, None, False),
|
||||||
|
("threshold", 0.5, 2.0, None, False),
|
||||||
|
("score", 0.5, 1.5, 0.05, False),
|
||||||
|
("size", 0.5, 2.0, None, False),
|
||||||
|
("lot", 0.5, 2.0, 0.01, False),
|
||||||
|
("spread", 0.5, 2.0, 1.0, True),
|
||||||
|
("trades", 0.5, 2.5, 1.0, True),
|
||||||
|
("hour", 0.0, 23.0, 1.0, True),
|
||||||
|
("session", 0.0, 23.0, 1.0, True),
|
||||||
|
]
|
||||||
|
match = None
|
||||||
|
for kw, *_ in patterns:
|
||||||
|
if kw in lower:
|
||||||
|
match = next(p for p in patterns if p[0] == kw)
|
||||||
|
break
|
||||||
|
|
||||||
|
if match:
|
||||||
|
_, lo_mul, hi_mul, step_override, force_int = match
|
||||||
|
lo = abs(v) * lo_mul
|
||||||
|
hi = abs(v) * hi_mul
|
||||||
|
if v < 0:
|
||||||
|
lo, hi = -hi, -lo
|
||||||
|
# Special-case hour ranges (absolute, not %-of-value)
|
||||||
|
if match[0] in ("hour", "session"):
|
||||||
|
lo, hi = 0.0, 23.0
|
||||||
|
else:
|
||||||
|
# Generic fallback: ±50% of value
|
||||||
|
lo = abs(v) * 0.5
|
||||||
|
hi = abs(v) * 2.0
|
||||||
|
if v < 0:
|
||||||
|
lo, hi = -hi, -lo
|
||||||
|
step_override = None
|
||||||
|
force_int = is_int_value
|
||||||
|
|
||||||
|
# Choose step
|
||||||
|
if step_override is not None:
|
||||||
|
step = step_override
|
||||||
|
elif force_int:
|
||||||
|
span = hi - lo
|
||||||
|
step = max(1.0, round(span / 10.0))
|
||||||
|
else:
|
||||||
|
span = hi - lo
|
||||||
|
step = round(span / 10.0, 4)
|
||||||
|
if step <= 0:
|
||||||
|
step = 0.01
|
||||||
|
|
||||||
|
ptype = "int" if force_int else "float"
|
||||||
|
default = int(round(v)) if force_int else v
|
||||||
|
|
||||||
|
return ParameterDef(
|
||||||
|
name=name, type=ptype, default=default,
|
||||||
|
min=round(lo, 4), max=round(hi, 4), step=step,
|
||||||
|
optimize=False,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user