Initial commit: MT5 EA Optimizer v1.0

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
This commit is contained in:
LEGSTECH Optimizer
2026-04-13 02:28:09 +00:00
commit 7a3e13a734
40 changed files with 7182 additions and 0 deletions
View File
+247
View File
@@ -0,0 +1,247 @@
"""
mutation/engine.py
Translates analysis findings into concrete parameter hypotheses.
Uses knowledge_base.yaml rules as a structured ruleset.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any, Optional
import numpy as np
import yaml
from loguru import logger
from data.models import Finding, Hypothesis
class MutationEngine:
"""
Finding → Hypothesis translator.
Workflow:
1. Load knowledge_base.yaml rules
2. For each finding, find matching rules
3. Filter rules already tested recently (dedup)
4. Resolve dynamic mutation values (percentile-based, derived)
5. Build Hypothesis objects
6. Return sorted by estimated PnL impact
"""
def __init__(
self,
kb_path: str | Path = "mutation/knowledge_base.yaml",
manifest_path: str | Path = "mutation/param_manifest.yaml",
dedup_lookback: int = 10,
):
with open(kb_path) as f:
self.kb = yaml.safe_load(f)["rules"]
with open(manifest_path) as f:
self.manifest = yaml.safe_load(f)["parameters"]
self.dedup_lookback = dedup_lookback
# ── Public ────────────────────────────────────────────────────────────────
def propose(
self,
findings: list[Finding],
current_params: dict[str, Any],
recent_deltas: list[dict], # from store.get_recent_param_deltas()
max_proposals: int = 3,
) -> list[Hypothesis]:
"""
Generate hypotheses from findings, de-duplicate, and return top-N.
"""
hypotheses: list[Hypothesis] = []
for finding in findings:
for rule in self.kb:
if not self._rule_matches(rule, finding, current_params):
continue
param_delta = self._build_delta(rule, finding, current_params)
if not param_delta:
continue
# Skip if identical delta was recently tested
if self._already_tested(param_delta, recent_deltas):
logger.debug(f"Skipping rule {rule['id']} — already tested.")
continue
h = Hypothesis(
parent_run_id=finding.run_id,
finding_ids=[finding.finding_id],
description=f"[{rule['id']}] {rule['action_label']}",
param_delta=param_delta,
strategy=rule.get("strategy", "targeted"),
kb_rule_id=rule["id"],
)
hypotheses.append((h, finding.impact_estimate_pnl))
# Sort by impact descending, deduplicate by KB rule
seen_rules = set()
ranked: list[Hypothesis] = []
for h, impact in sorted(hypotheses, key=lambda x: x[1], reverse=True):
if h.kb_rule_id not in seen_rules:
seen_rules.add(h.kb_rule_id)
ranked.append(h)
if len(ranked) >= max_proposals:
break
logger.info(f"Proposed {len(ranked)} hypotheses from {len(findings)} findings.")
return ranked
# ── Rule matching ─────────────────────────────────────────────────────────
def _rule_matches(
self, rule: dict, finding: Finding, current_params: dict
) -> bool:
"""Check if a KB rule's trigger matches this finding and current params."""
trigger = rule.get("trigger", {})
# Analyzer match
if trigger.get("analyzer") and trigger["analyzer"] != finding.analyzer:
return False
# Evaluate condition expression against finding evidence + current params
condition = trigger.get("condition", "")
if condition:
env = {**finding.evidence, **current_params}
# Simple boolean parsing for conditions like "reversal_rate > 0.15"
try:
if not self._eval_condition(condition, env):
return False
except Exception as e:
logger.debug(f"Rule {rule['id']} condition eval error: {e}")
return False
return True
def _eval_condition(self, condition: str, env: dict) -> bool:
"""
Evaluate a simple condition string.
Supports: >, <, >=, <=, ==, AND, OR
Variables are looked up in env dict.
"""
# Replace variable names with their values
tokens = condition.split()
resolved_tokens = []
for token in tokens:
if token in ("AND", "OR", "and", "or", ">", "<", ">=", "<=", "==", "!="):
resolved_tokens.append(token.lower())
elif token in env:
val = env[token]
resolved_tokens.append(str(val) if not isinstance(val, str) else f'"{val}"')
else:
resolved_tokens.append(token)
expr = " ".join(resolved_tokens)
return bool(eval(expr, {"__builtins__": {}})) # restricted eval
# ── Delta building ────────────────────────────────────────────────────────
def _build_delta(
self,
rule: dict,
finding: Finding,
current_params: dict,
) -> dict[str, Any]:
"""
Translate a KB mutation spec into a concrete {param_name: new_value} dict.
Handles: set, multiply, derive_from, set_to_percentile.
"""
delta: dict[str, Any] = {}
mutations = rule.get("mutations", {})
for param_name, mutation_spec in mutations.items():
current = current_params.get(param_name)
spec = self.manifest.get(param_name, {})
ptype = spec.get("type", "float")
p_min = spec.get("min")
p_max = spec.get("max")
new_val = self._resolve_mutation(
mutation_spec, current, ptype, p_min, p_max, finding
)
if new_val is not None:
delta[param_name] = new_val
# Auto-cascade: if enabling a bool, set defaults for depends_on params
if ptype == "bool" and new_val is True:
delta.update(self._cascade_dependencies(param_name, current_params))
return delta
def _resolve_mutation(
self,
spec: dict | Any,
current: Any,
ptype: str,
p_min: Optional[float],
p_max: Optional[float],
finding: Finding,
) -> Optional[Any]:
"""Resolve a single mutation spec into a concrete value."""
if not isinstance(spec, dict):
return spec # bare value
# set: directly set to a value
if "set" in spec:
return spec["set"]
# multiply: multiply current value by factor
if "multiply" in spec and current is not None:
result = float(current) * spec["multiply"]
if "clamp_min" in spec:
result = max(spec["clamp_min"], result)
if p_min is not None:
result = max(p_min, result)
if p_max is not None:
result = min(p_max, result)
return round(result, 2) if ptype == "float" else int(result)
# set_to_percentile: use Nth percentile of a finding evidence list
if "set_to_percentile" in spec:
pct = spec["set_to_percentile"] / 100.0
data = finding.evidence.get("mfe_pips_distribution", [])
if data:
val = float(np.percentile(data, pct * 100))
if "scale" in spec:
val *= spec["scale"]
if p_min is not None:
val = max(p_min, val)
if p_max is not None:
val = min(p_max, val)
return round(val, 1)
# derive_from: use evidence field
if "derive_from" in spec:
key = spec["derive_from"]
val = finding.evidence.get(key)
if val is not None:
return val
return None
def _cascade_dependencies(
self, bool_param: str, current_params: dict
) -> dict[str, Any]:
"""When a bool param is enabled, fill in sensible defaults for its dependents."""
cascade = {}
for name, spec in self.manifest.items():
if spec.get("depends_on") != bool_param:
continue
# Only set if not already in current params or at a sub-optimal default
if name not in current_params:
cascade[name] = spec.get("default", 0)
return cascade
# ── Deduplication ─────────────────────────────────────────────────────────
def _already_tested(self, delta: dict, recent_deltas: list[dict]) -> bool:
"""Check if an identical param delta was tested recently."""
delta_str = json.dumps(delta, sort_keys=True)
for past in recent_deltas:
if json.dumps(past, sort_keys=True) == delta_str:
return True
return False
+187
View File
@@ -0,0 +1,187 @@
rules:
# ── Trailing / Exit Rules ────────────────────────────────────────────────────
- id: KB001
trigger:
analyzer: reversal
condition: "reversal_rate > 0.15"
action_label: "Enable trailing stop to protect in-profit trades (reversal rate high)"
mutations:
InpUseTrailing:
set: true
InpTrailStartPips:
derive_from: "mfe_p25" # 25th percentile of reversal MFE
fallback: 20.0
InpTrailStepPips:
set: 10.0
strategy: targeted
- id: KB002
trigger:
analyzer: reversal
condition: "reversal_rate > 0.20"
action_label: "Tighten TP ratio — too many trades reversing before target hit"
mutations:
InpRRRatio:
multiply: 0.80
clamp_min: 1.0
strategy: targeted
- id: KB003
trigger:
analyzer: reversal
condition: "mean_capture_ratio < 0.55"
action_label: "Low MFE capture on winners — enable or tighten trailing"
mutations:
InpUseTrailing:
set: true
InpTrailStartPips:
set: 15.0
InpTrailStepPips:
set: 8.0
strategy: targeted
# ── Session / Time Filter Rules ───────────────────────────────────────────────
- id: KB004
trigger:
analyzer: time_performance
condition: "type == 'hour_window'"
action_label: "Exclude identified negative-edge UTC time window via session filter"
mutations:
InpUseSession:
set: true
InpSessionEnd:
derive_from: "broker_start" # end session before bad window starts
strategy: targeted
- id: KB005
trigger:
analyzer: time_performance
condition: "type == 'session'"
action_label: "Negative-edge session detected — tighten or disable session window"
mutations:
InpUseSession:
set: true
strategy: targeted
# ── Entry Quality Rules ────────────────────────────────────────────────────────
- id: KB006
trigger:
analyzer: entry_exit_quality
condition: "diagnosis == 'poor_entry'"
action_label: "Poor entry quality — tighten ATR filter and score gate"
mutations:
InpUseSpreadGuard:
set: true
InpMinScore:
multiply: 1.125
clamp_min: 6
InpATRMultiplier:
multiply: 1.20
clamp_min: 0.3
strategy: targeted
- id: KB007
trigger:
analyzer: entry_exit_quality
condition: "diagnosis == 'good_entry_poor_exit'"
action_label: "Good entries, poor exits — enable trailing with conservative start"
mutations:
InpUseTrailing:
set: true
InpTrailStartPips:
set: 18.0
InpTrailStepPips:
set: 10.0
strategy: targeted
- id: KB008
trigger:
analyzer: entry_exit_quality
condition: "diagnosis == 'both_broken'"
action_label: "Both entry and exit quality poor — test conservative bot mode"
mutations:
InpBotMode:
set: 2
InpMinScore:
multiply: 1.25
clamp_min: 6
strategy: compound
# ── Risk / Drawdown Rules ─────────────────────────────────────────────────────
- id: KB009
trigger:
analyzer: equity_curve
condition: "flatness_score > 0.50"
action_label: "Equity spending too much time in drawdown — reduce risk per trade"
mutations:
InpRiskPercent:
multiply: 0.75
clamp_min: 0.5
InpMaxDailyLossPct:
multiply: 0.80
clamp_min: 1.0
strategy: targeted
- id: KB010
trigger:
analyzer: equity_curve
condition: "cluster_count > 3"
action_label: "Repeated loss clusters — limit consecutive trades and daily risk"
mutations:
InpMaxTradesPerDay:
multiply: 0.75
clamp_min: 2
InpMaxDailyLossPct:
set: 2.0
strategy: targeted
# ── Breakeven Rules ────────────────────────────────────────────────────────────
- id: KB011
trigger:
analyzer: reversal
condition: "reversal_rate > 0.12"
action_label: "Enable breakeven stop to lock in partial profit before reversal"
mutations:
InpUseBreakeven:
set: true
InpBEPips:
derive_from: "mfe_p25"
fallback: 15.0
InpBEBufferPips:
set: 2.0
strategy: targeted
# ── Filter Tightening Rules ────────────────────────────────────────────────────
- id: KB012
trigger:
analyzer: entry_exit_quality
condition: "high_mae_loser_count > 10"
action_label: "High MAE losers — require EMA slope confirmation"
mutations:
InpUseEMA:
set: true
InpRequireEMASlope:
set: true
InpEMASlopeBars:
set: 2
strategy: targeted
- id: KB013
trigger:
analyzer: entry_exit_quality
condition: "mean_entry_quality < 0.35"
action_label: "Very poor entry quality — tighten minimum RR gate"
mutations:
InpUseMinRR:
set: true
InpMinRRRatio:
multiply: 1.25
clamp_min: 1.0
strategy: targeted
+394
View File
@@ -0,0 +1,394 @@
# Parameter Manifest — LEGSTECH_EA_V2
# Generated from: LEGSTECH_EA_V2.set
# Format: value = default, min/max/step = optimization bounds
# Types: float | int | bool | enum
# ─────────────────────────────────────────────────────────────
parameters:
# ── Bot / Mode ─────────────────────────────────────────────
InpBotMode:
type: enum
values: [0, 1, 2] # 0, 1, 2 per .set range 0→2 step 1
default: 1
category: mode
description: "Bot operating mode"
# ── Timeframe Selection (PERIOD_ codes) ────────────────────
# These are MT5 ENUM_TIMEFRAMES integer codes. Not optimized — fixed.
InpHTF:
type: fixed
default: 16408 # PERIOD_H4
category: timeframe
InpMTF:
type: fixed
default: 16388 # PERIOD_H1
category: timeframe
InpLTF:
type: fixed
default: 16385 # PERIOD_M30
category: timeframe
# ── Risk Management ────────────────────────────────────────
InpRiskType:
type: enum
values: [0, 1] # 0=fixed lot, 1=percent risk
default: 1
category: risk
InpFixedLot:
type: float
min: 0.01
max: 1.0
step: 0.01
default: 0.01
category: risk
depends_on_value: {InpRiskType: 0} # only active when using fixed lot mode
InpRiskPercent:
type: float
min: 0.5
max: 3.0
step: 0.5
default: 1.0
category: risk
depends_on_value: {InpRiskType: 1} # only active when using percent risk mode
InpMaxDailyLossPct:
type: float
min: 1.0
max: 5.0
step: 0.5
default: 3.0
category: risk
InpMaxTradesPerDay:
type: int
min: 1
max: 10
step: 1
default: 5
category: risk
# ── Stop Loss ──────────────────────────────────────────────
InpSLType:
type: enum
values: [0, 1] # 0=fixed pips, 1=ATR-based
default: 0
category: sl
InpSLBuffer:
type: float
min: 5.0
max: 30.0
step: 5.0
default: 10.0
category: sl
description: "Buffer pips added to SL"
InpFixedSLPips:
type: float
min: 50.0
max: 200.0
step: 10.0
default: 100.0
category: sl
depends_on_value: {InpSLType: 0}
InpMaxSLPips:
type: float
min: 100.0
max: 400.0
step: 50.0
default: 200.0
category: sl
description: "Hard cap on calculated SL size"
InpUseFractalSL:
type: bool
default: false # 0 in .set
category: sl
# ── Take Profit ────────────────────────────────────────────
InpTPType:
type: enum
values: [0, 1] # 0=RR ratio, 1=fixed pips
default: 0
category: tp
InpRRRatio:
type: float
min: 1.0
max: 3.0
step: 0.5
default: 1.5
category: tp
depends_on_value: {InpTPType: 0}
InpFixedTPPips:
type: float
min: 50.0
max: 200.0
step: 10.0
default: 100.0
category: tp
depends_on_value: {InpTPType: 1}
InpUseFractalFilter:
type: bool
default: false # 0 in .set
category: tp
# ── Session Filter ─────────────────────────────────────────
InpUseSession:
type: bool
default: true # 1 in .set
category: filter_session
InpSessionStart:
type: int
min: 0
max: 23
step: 1
default: 7
category: filter_session
depends_on: InpUseSession
description: "Session start hour (broker local time)"
InpSessionEnd:
type: int
min: 0
max: 23
step: 1
default: 20
category: filter_session
depends_on: InpUseSession
description: "Session end hour (broker local time)"
# ── Trade Limits ───────────────────────────────────────────
InpMaxOpenTrades:
type: int
min: 1
max: 3
step: 1
default: 1
category: risk
InpAllowMultiple:
type: bool
default: false # 0 in .set
category: risk
# ── Execution ─────────────────────────────────────────────
InpMagicNumber:
type: fixed
default: 202402
category: execution
description: "Fixed — do not optimize"
InpSlippage:
type: int
min: 5
max: 30
step: 5
default: 10
category: execution
# ── Trailing Stop ─────────────────────────────────────────
InpUseTrailing:
type: bool
default: true # 1 in .set
category: exit_trail
InpTrailStartPips:
type: float
min: 10.0
max: 50.0
step: 5.0
default: 20.0
category: exit_trail
depends_on: InpUseTrailing
InpTrailStepPips:
type: float
min: 5.0
max: 30.0
step: 5.0
default: 10.0
category: exit_trail
depends_on: InpUseTrailing
# ── Break Even ────────────────────────────────────────────
InpUseBreakeven:
type: bool
default: true # 1 in .set
category: exit_be
InpBEPips:
type: float
min: 10.0
max: 40.0
step: 5.0
default: 15.0
category: exit_be
depends_on: InpUseBreakeven
description: "Pips in profit to activate breakeven"
InpBEBufferPips:
type: float
min: 1.0
max: 5.0
step: 1.0
default: 2.0
category: exit_be
depends_on: InpUseBreakeven
description: "Buffer pips above entry for breakeven SL"
# ── EMA Filter ────────────────────────────────────────────
InpUseEMA:
type: bool
default: true # 1 in .set
category: filter_ema
InpEMAPeriod:
type: int
min: 20
max: 100
step: 10
default: 50
category: filter_ema
depends_on: InpUseEMA
InpRequireEMASlope:
type: bool
default: true # 1 in .set
category: filter_ema
depends_on: InpUseEMA
InpEMASlopeBars:
type: int
min: 1
max: 3
step: 1
default: 1
category: filter_ema
depends_on: InpRequireEMASlope
# ── Entry Mode ────────────────────────────────────────────
InpEntryMode:
type: enum
values: [0, 1]
default: 1
category: entry
InpSLBufferMode:
type: enum
values: [0, 1]
default: 1
category: sl
# ── ATR ───────────────────────────────────────────────────
InpATRPeriod:
type: int
min: 10
max: 20
step: 2
default: 14
category: atr
InpATRMultiplier:
type: float
min: 0.3
max: 1.0
step: 0.1
default: 0.5
category: atr
# ── Spread Guard ──────────────────────────────────────────
InpUseSpreadGuard:
type: bool
default: true # 1 in .set
category: filter_spread
InpMaxSpreadPips:
type: float
min: 10.0
max: 50.0
step: 5.0
default: 30.0
category: filter_spread
depends_on: InpUseSpreadGuard
# ── Minimum R:R Gate ──────────────────────────────────────
InpUseMinRR:
type: bool
default: true # 1 in .set
category: filter_rr
InpMinRRRatio:
type: float
min: 1.0
max: 3.0
step: 0.5
default: 1.5
category: filter_rr
depends_on: InpUseMinRR
# ── Score Gate ────────────────────────────────────────────
InpUseScoreGate:
type: bool
default: true # 1 in .set
category: filter_score
InpMinScore:
type: int
min: 6
max: 11
step: 1
default: 8
category: filter_score
depends_on: InpUseScoreGate
description: "Minimum signal quality score required to enter trade"
# ── Tester-Specific (fixed during automation) ─────────────
InpTesterMode:
type: fixed
default: 1
category: tester
description: "Must be 1 during automated backtesting"
InpTesterInitDeposit:
type: fixed
default: 10000.0
category: tester
InpTesterSpreadPts:
type: int
min: 10
max: 50
step: 5
default: 20
category: tester
description: "Spread in points used in tester (20 pts = 2.0 pips for XAUUSD)"
InpShowPanel:
type: fixed
default: 0 # force off during automation (no GUI needed)
category: tester
# ── Parameter categories (for mutation engine grouping) ──────
categories:
mode: [InpBotMode]
risk: [InpRiskType, InpFixedLot, InpRiskPercent, InpMaxDailyLossPct, InpMaxTradesPerDay, InpMaxOpenTrades, InpAllowMultiple]
sl: [InpSLType, InpSLBuffer, InpFixedSLPips, InpMaxSLPips, InpUseFractalSL, InpSLBufferMode]
tp: [InpTPType, InpRRRatio, InpFixedTPPips, InpUseFractalFilter]
exit_trail: [InpUseTrailing, InpTrailStartPips, InpTrailStepPips]
exit_be: [InpUseBreakeven, InpBEPips, InpBEBufferPips]
filter_session: [InpUseSession, InpSessionStart, InpSessionEnd]
filter_ema: [InpUseEMA, InpEMAPeriod, InpRequireEMASlope, InpEMASlopeBars]
filter_spread: [InpUseSpreadGuard, InpMaxSpreadPips]
filter_rr: [InpUseMinRR, InpMinRRRatio]
filter_score: [InpUseScoreGate, InpMinScore]
entry: [InpEntryMode]
atr: [InpATRPeriod, InpATRMultiplier]
tester: [InpTesterMode, InpTesterInitDeposit, InpTesterSpreadPts, InpShowPanel]