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
558 lines
23 KiB
Python
558 lines
23 KiB
Python
"""
|
|
main.py
|
|
MT5 EA Strategy Optimizer — CLI Entry Point
|
|
LEGSTECH_EA_V2 | XAUUSD | H1
|
|
|
|
Usage:
|
|
python main.py # interactive mode
|
|
python main.py --baseline # run baseline only and show analysis
|
|
python main.py --auto # fully automated loop (no human prompts)
|
|
"""
|
|
from __future__ import annotations
|
|
import argparse
|
|
import sys
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Optional, Any
|
|
|
|
import yaml
|
|
import pandas as pd
|
|
from loguru import logger
|
|
from rich.console import Console
|
|
from rich.table import Table
|
|
from rich.panel import Panel
|
|
from rich.prompt import Confirm, Prompt
|
|
from rich import print as rprint
|
|
|
|
# ── Project imports ──────────────────────────────────────────────────────────
|
|
from data.models import Run, RunMetrics, Candidate
|
|
from data.store import DataStore
|
|
from mt5.ini_builder import IniBuilder
|
|
from mt5.runner import MT5Runner
|
|
from mt5.report_parser import ReportParser
|
|
from mt5.log_reader import TradeLogReader
|
|
from analysis.reversal import ReversalAnalyzer
|
|
from analysis.time_performance import TimePerformanceAnalyzer
|
|
from analysis.entry_exit_quality import EntryExitQualityAnalyzer
|
|
from analysis.equity_curve import EquityCurveAnalyzer
|
|
from scoring.composite import CompositeScorer
|
|
from mutation.engine import MutationEngine
|
|
from validation.gate import ValidationGate
|
|
|
|
console = Console()
|
|
|
|
CONFIG_PATH = Path("config.yaml")
|
|
MANIFEST_PATH = Path("mutation/param_manifest.yaml")
|
|
KB_PATH = Path("mutation/knowledge_base.yaml")
|
|
DB_PATH = Path("optimizer.db")
|
|
RUNS_DIR = Path("runs")
|
|
|
|
|
|
def load_config() -> dict:
|
|
with open(CONFIG_PATH) as f:
|
|
return yaml.safe_load(f)
|
|
|
|
|
|
# ── Component factory ─────────────────────────────────────────────────────────
|
|
|
|
def build_components(cfg: dict):
|
|
store = DataStore(DB_PATH, RUNS_DIR)
|
|
builder = IniBuilder(CONFIG_PATH, MANIFEST_PATH)
|
|
runner = MT5Runner(CONFIG_PATH)
|
|
parser = ReportParser()
|
|
log_rdr = TradeLogReader(
|
|
broker_tz_offset_hours=cfg["broker"]["timezone_offset_hours"],
|
|
pip_size=0.1, # XAUUSD: 0.1 per pip
|
|
)
|
|
analyzers = [
|
|
ReversalAnalyzer(
|
|
mfe_threshold_pips=cfg["analysis"]["reversal"]["mfe_threshold_pips"],
|
|
min_reversal_rate=cfg["analysis"]["reversal"]["min_reversal_rate"],
|
|
permutation_n=cfg["analysis"]["reversal"]["permutation_n"],
|
|
),
|
|
TimePerformanceAnalyzer(
|
|
z_score_threshold=cfg["analysis"]["time_performance"]["z_score_threshold"],
|
|
min_bucket_trades=cfg["analysis"]["time_performance"]["min_trades_per_bucket"],
|
|
permutation_n=cfg["analysis"]["time_performance"]["permutation_n"],
|
|
),
|
|
EntryExitQualityAnalyzer(
|
|
poor_exit_threshold=cfg["analysis"]["entry_exit"]["poor_exit_quality"],
|
|
poor_entry_threshold=cfg["analysis"]["entry_exit"]["poor_entry_quality"],
|
|
),
|
|
EquityCurveAnalyzer(
|
|
max_flatness=cfg["analysis"]["equity_curve"]["max_flatness_score"],
|
|
min_r_squared=cfg["analysis"]["equity_curve"]["min_r_squared"],
|
|
),
|
|
]
|
|
scorer = CompositeScorer(str(CONFIG_PATH))
|
|
mutator = MutationEngine(KB_PATH, MANIFEST_PATH,
|
|
dedup_lookback=cfg["mutation"]["dedup_lookback_runs"])
|
|
gate = ValidationGate(CONFIG_PATH)
|
|
return store, builder, runner, parser, log_rdr, analyzers, scorer, mutator, gate
|
|
|
|
|
|
# ── Single run pipeline ───────────────────────────────────────────────────────
|
|
|
|
def execute_run(
|
|
run_id: str,
|
|
params: dict[str, Any],
|
|
period_start: str,
|
|
period_end: str,
|
|
phase: str,
|
|
hypothesis_id: Optional[str],
|
|
cfg: dict,
|
|
store: DataStore,
|
|
builder: IniBuilder,
|
|
runner: MT5Runner,
|
|
parser: ReportParser,
|
|
log_rdr: TradeLogReader,
|
|
analyzers: list,
|
|
scorer: CompositeScorer,
|
|
) -> tuple[Optional[RunMetrics], pd.DataFrame]:
|
|
"""
|
|
Execute one complete backtest run:
|
|
1. Build INI → Launch MT5 → Wait → Parse report → Merge logger CSV
|
|
2. Compute derived fields, enrich trades
|
|
3. Compute composite score
|
|
4. Save everything to store
|
|
Returns (metrics, trades_df) or (None, empty_df) on failure.
|
|
"""
|
|
run_dir = RUNS_DIR / run_id
|
|
run_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 1. Build INI
|
|
ini_path = builder.build(
|
|
run_id=run_id,
|
|
params=params,
|
|
period_start=period_start,
|
|
period_end=period_end,
|
|
output_dir=run_dir,
|
|
phase=phase,
|
|
)
|
|
|
|
# 2. Save run record
|
|
run = Run(
|
|
run_id=run_id,
|
|
ea_name=cfg["ea"]["name"],
|
|
symbol=cfg["ea"]["symbol"],
|
|
timeframe=cfg["ea"]["timeframe"],
|
|
period_start=period_start,
|
|
period_end=period_end,
|
|
params=params,
|
|
phase=phase,
|
|
hypothesis_id=hypothesis_id,
|
|
tester_model=cfg["mt5"]["tester_model"],
|
|
ini_snapshot=ini_path.read_text(),
|
|
)
|
|
store.save_run(run)
|
|
|
|
# 3. Launch MT5
|
|
report_dir = run_dir / "report"
|
|
# MT5 Files folder: may need adjusting to actual terminal data path
|
|
mt5_files_dir = None # TODO: set to actual MQL5/Files path once terminal path confirmed
|
|
|
|
console.print(f"[dim]Launching MT5... (timeout {cfg['mt5']['tester_timeout_seconds']}s)[/dim]")
|
|
result = runner.run(run_id, ini_path, report_dir, log_csv_search_dir=mt5_files_dir)
|
|
|
|
if not result.success:
|
|
logger.error(f"Run {run_id} failed: {result.error_message}")
|
|
console.print(f"[red]✗ MT5 run failed: {result.error_message}[/red]")
|
|
return None, pd.DataFrame()
|
|
|
|
# 4. Parse report
|
|
metrics, trades = parser.parse(result.report_xml, result.report_html)
|
|
if metrics is None:
|
|
console.print(f"[red]✗ Could not parse report for {run_id}[/red]")
|
|
return None, pd.DataFrame()
|
|
metrics.run_id = run_id
|
|
|
|
# 5. Merge TradeLogger CSV → enrich with MAE/MFE + derived fields
|
|
if trades:
|
|
trades = log_rdr.merge(
|
|
trades, result.trade_log_csv,
|
|
reversal_mfe_threshold_pips=cfg["analysis"]["reversal"]["mfe_threshold_pips"],
|
|
)
|
|
trades_df = pd.DataFrame([t.model_dump() for t in trades])
|
|
else:
|
|
trades_df = pd.DataFrame()
|
|
|
|
# 6. Compute reversal rate and MFE capture for metrics
|
|
if not trades_df.empty:
|
|
if "result_class" in trades_df.columns:
|
|
losers = trades_df[trades_df["net_money"] < 0]
|
|
reversals = trades_df[trades_df["result_class"] == "reversal"]
|
|
metrics.reversal_rate = (
|
|
len(reversals) / max(1, len(losers))
|
|
)
|
|
if "mfe_capture_ratio" in trades_df.columns:
|
|
metrics.avg_mfe_capture = float(
|
|
trades_df["mfe_capture_ratio"].dropna().mean() or 0
|
|
)
|
|
if "mfe_pips" in trades_df.columns:
|
|
metrics.avg_mfe_pips = float(trades_df["mfe_pips"].dropna().mean() or 0)
|
|
metrics.avg_mae_pips = float(trades_df.get("mae_pips", pd.Series()).dropna().mean() or 0)
|
|
|
|
# 7. Compute composite score (session stats can be added here in v2)
|
|
metrics.composite_score = scorer.score(metrics)
|
|
|
|
# 8. Persist
|
|
store.save_metrics(metrics)
|
|
if not trades_df.empty:
|
|
store.save_trades(run_id, trades)
|
|
run.report_path = result.report_xml
|
|
run.log_csv_path = result.trade_log_csv
|
|
store.save_run(run)
|
|
|
|
return metrics, trades_df
|
|
|
|
|
|
# ── Analysis pipeline ─────────────────────────────────────────────────────────
|
|
|
|
def run_analysis(
|
|
run_id: str,
|
|
trades_df: pd.DataFrame,
|
|
metrics: RunMetrics,
|
|
analyzers: list,
|
|
store: DataStore,
|
|
) -> list:
|
|
"""Run all analyzers and persist findings."""
|
|
all_findings = []
|
|
for analyzer in analyzers:
|
|
findings = analyzer.run(trades_df, metrics, run_id)
|
|
all_findings.extend(findings)
|
|
console.print(
|
|
f" [green]✓[/green] {analyzer.name:<25} → {len(findings)} finding(s)"
|
|
)
|
|
|
|
all_findings.sort(key=lambda f: f.confidence, reverse=True)
|
|
store.save_findings(all_findings)
|
|
return all_findings
|
|
|
|
|
|
# ── Display helpers ───────────────────────────────────────────────────────────
|
|
|
|
def display_metrics(metrics: RunMetrics, label: str = "Backtest Results") -> None:
|
|
table = Table(title=label, show_header=True, header_style="bold cyan")
|
|
table.add_column("Metric", style="dim")
|
|
table.add_column("Value", justify="right")
|
|
|
|
table.add_row("Net Profit", f"${metrics.net_profit:,.2f}")
|
|
table.add_row("Profit Factor", f"{metrics.profit_factor:.3f}")
|
|
table.add_row("Calmar Ratio", f"{metrics.calmar_ratio:.3f}")
|
|
table.add_row("Max Drawdown", f"{metrics.max_drawdown_pct*100:.1f}% (${metrics.max_drawdown_abs:,.0f})")
|
|
table.add_row("Total Trades", str(metrics.total_trades))
|
|
table.add_row("Win Rate", f"{metrics.win_rate*100:.1f}%")
|
|
table.add_row("Sharpe", f"{metrics.sharpe_ratio:.3f}")
|
|
table.add_row("Recovery Factor", f"{metrics.recovery_factor:.3f}")
|
|
if metrics.avg_mfe_capture is not None:
|
|
table.add_row("MFE Capture", f"{metrics.avg_mfe_capture*100:.1f}%")
|
|
if metrics.reversal_rate is not None:
|
|
table.add_row("Reversal Rate", f"{metrics.reversal_rate*100:.1f}%")
|
|
table.add_row("Composite Score", f"[bold]{metrics.composite_score:.4f}[/bold]")
|
|
|
|
console.print(table)
|
|
|
|
|
|
def display_findings(findings: list) -> None:
|
|
table = Table(title="Analysis Findings", show_header=True, header_style="bold yellow")
|
|
table.add_column("#", width=3)
|
|
table.add_column("Finding", max_width=60)
|
|
table.add_column("Severity", width=8)
|
|
table.add_column("Confidence", width=10, justify="right")
|
|
table.add_column("Est. Impact", width=12, justify="right")
|
|
|
|
sev_colors = {"high": "red", "medium": "yellow", "low": "dim"}
|
|
|
|
for i, f in enumerate(findings, 1):
|
|
color = sev_colors.get(f.severity, "white")
|
|
table.add_row(
|
|
str(i),
|
|
f.description[:60] + ("…" if len(f.description) > 60 else ""),
|
|
f"[{color}]{f.severity.upper()}[/{color}]",
|
|
f"{f.confidence:.2f}",
|
|
f"${f.impact_estimate_pnl:,.0f}",
|
|
)
|
|
console.print(table)
|
|
|
|
|
|
def display_hypotheses(hypotheses: list, current_params: dict) -> None:
|
|
table = Table(title="Proposed Hypotheses", show_header=True, header_style="bold magenta")
|
|
table.add_column("#", width=3)
|
|
table.add_column("Description", max_width=50)
|
|
table.add_column("Parameter Changes", max_width=40)
|
|
|
|
for i, h in enumerate(hypotheses, 1):
|
|
changes = []
|
|
for param, val in h.param_delta.items():
|
|
old = current_params.get(param, "?")
|
|
changes.append(f"{param}: {old} → {val}")
|
|
table.add_row(
|
|
str(i),
|
|
h.description[:50],
|
|
"\n".join(changes),
|
|
)
|
|
console.print(table)
|
|
|
|
|
|
# ── Main loop ─────────────────────────────────────────────────────────────────
|
|
|
|
def main(auto_mode: bool = False, baseline_only: bool = False) -> None:
|
|
cfg = load_config()
|
|
store, builder, runner, parser, log_rdr, analyzers, scorer, mutator, gate = (
|
|
build_components(cfg)
|
|
)
|
|
|
|
ea = cfg["ea"]
|
|
per = cfg["periods"]
|
|
|
|
console.print(Panel(
|
|
f"[bold cyan]MT5 EA Strategy Optimizer[/bold cyan]\n"
|
|
f"EA: {ea['name']} | Symbol: {ea['symbol']} | TF: {ea['timeframe']}\n"
|
|
f"Train: {per['train_start']} → {per['train_end']} | "
|
|
f"OOS: {per['oos_start']} → {per['oos_end']} [bold red](LOCKED)[/bold red]",
|
|
title="[bold]Session Start[/bold]",
|
|
))
|
|
|
|
# ── PHASE 0: Baseline ─────────────────────────────────────────────────────
|
|
console.rule("[bold]Phase 0: Baseline Run[/bold]")
|
|
default_params = builder.default_params()
|
|
baseline_id = f"baseline_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}"
|
|
|
|
baseline_metrics, baseline_trades = execute_run(
|
|
run_id=baseline_id,
|
|
params=default_params,
|
|
period_start=per["train_start"],
|
|
period_end=per["train_end"],
|
|
phase="baseline",
|
|
hypothesis_id=None,
|
|
cfg=cfg, store=store, builder=builder, runner=runner,
|
|
parser=parser, log_rdr=log_rdr, analyzers=analyzers, scorer=scorer,
|
|
)
|
|
|
|
if baseline_metrics is None:
|
|
console.print("[red]Baseline run failed. Check MT5 config and terminal path.[/red]")
|
|
sys.exit(1)
|
|
|
|
display_metrics(baseline_metrics, label="Baseline Results")
|
|
|
|
if baseline_only:
|
|
# ── Analysis only ────────────────────────────────────────────────────
|
|
console.rule("[bold]Analysis[/bold]")
|
|
findings = run_analysis(baseline_id, baseline_trades, baseline_metrics,
|
|
analyzers, store)
|
|
display_findings(findings)
|
|
return
|
|
|
|
current_params = default_params.copy()
|
|
current_metrics = baseline_metrics
|
|
best_score = baseline_metrics.composite_score
|
|
iteration = 0
|
|
no_improvement_count = 0
|
|
|
|
max_iter = cfg["optimization"]["max_iterations"]
|
|
conv_win = cfg["optimization"]["convergence_window"]
|
|
conv_thr = cfg["optimization"]["convergence_threshold"]
|
|
|
|
# ── Iteration loop ────────────────────────────────────────────────────────
|
|
while iteration < max_iter:
|
|
iteration += 1
|
|
console.rule(f"[bold]Iteration {iteration}[/bold]")
|
|
|
|
# Analysis
|
|
console.print("[bold]Running analysis modules...[/bold]")
|
|
parent_run_id = baseline_id if iteration == 1 else f"iter_{iteration-1}"
|
|
trades_df = store.load_trades(
|
|
baseline_id if iteration == 1 else f"iter_{iteration-1}_best"
|
|
)
|
|
if trades_df.empty:
|
|
trades_df = baseline_trades
|
|
|
|
findings = run_analysis(
|
|
baseline_id, trades_df, current_metrics, analyzers, store
|
|
)
|
|
display_findings(findings[:8]) # top 8
|
|
|
|
if not findings:
|
|
console.print("[yellow]No actionable findings. Stopping.[/yellow]")
|
|
break
|
|
|
|
# Mutation
|
|
recent_deltas = store.get_recent_param_deltas(cfg["mutation"]["dedup_lookback_runs"])
|
|
hypotheses = mutator.propose(
|
|
findings=findings,
|
|
current_params=current_params,
|
|
recent_deltas=recent_deltas,
|
|
max_proposals=cfg["mutation"]["max_hypotheses_per_cycle"],
|
|
)
|
|
|
|
if not hypotheses:
|
|
console.print("[yellow]No new hypotheses available. Stopping.[/yellow]")
|
|
break
|
|
|
|
display_hypotheses(hypotheses, current_params)
|
|
|
|
# Human approval (skipped in auto mode)
|
|
if auto_mode:
|
|
selected_indices = list(range(len(hypotheses)))
|
|
else:
|
|
choice = Prompt.ask(
|
|
"Apply which hypotheses?",
|
|
default="1",
|
|
)
|
|
if choice.lower() in ("skip", "s", ""):
|
|
console.print("[dim]Skipping...[/dim]")
|
|
continue
|
|
if choice.lower() == "all":
|
|
selected_indices = list(range(len(hypotheses)))
|
|
else:
|
|
selected_indices = [int(x.strip()) - 1 for x in choice.split(",")]
|
|
|
|
# Test selected hypotheses
|
|
iteration_best: Optional[RunMetrics] = None
|
|
iteration_best_params: Optional[dict] = None
|
|
iteration_best_hyp = None
|
|
|
|
for idx in selected_indices:
|
|
if idx < 0 or idx >= len(hypotheses):
|
|
continue
|
|
hyp = hypotheses[idx]
|
|
test_params = {**current_params, **hyp.param_delta}
|
|
run_id = f"iter_{iteration}_h{idx+1}"
|
|
|
|
console.print(f"\n[bold]Testing hypothesis {idx+1}: {hyp.description}[/bold]")
|
|
store.save_hypothesis(hyp)
|
|
|
|
test_metrics, test_trades = execute_run(
|
|
run_id=run_id,
|
|
params=test_params,
|
|
period_start=per["train_start"],
|
|
period_end=per["train_end"],
|
|
phase="explore",
|
|
hypothesis_id=hyp.hypothesis_id,
|
|
cfg=cfg, store=store, builder=builder, runner=runner,
|
|
parser=parser, log_rdr=log_rdr, analyzers=analyzers, scorer=scorer,
|
|
)
|
|
|
|
if test_metrics is None:
|
|
continue
|
|
|
|
display_metrics(test_metrics, label=f"H{idx+1} Results")
|
|
delta_score = test_metrics.composite_score - current_metrics.composite_score
|
|
color = "green" if delta_score > 0 else "red"
|
|
console.print(
|
|
f"Score delta: [{color}]{delta_score:+.4f}[/{color}] "
|
|
f"({current_metrics.composite_score:.4f} → {test_metrics.composite_score:.4f})"
|
|
)
|
|
|
|
store.update_hypothesis_status(hyp.hypothesis_id, "tested", run_id)
|
|
|
|
if iteration_best is None or test_metrics.composite_score > iteration_best.composite_score:
|
|
iteration_best = test_metrics
|
|
iteration_best_params = test_params
|
|
iteration_best_hyp = hyp
|
|
|
|
if iteration_best is None:
|
|
console.print("[red]All hypotheses failed to run.[/red]")
|
|
continue
|
|
|
|
# Validation gate
|
|
gate_result = gate.run_is_check(iteration_best)
|
|
if not gate_result.passed:
|
|
console.print(f"[red]IS gate failed: {gate_result.details}[/red]")
|
|
store.update_hypothesis_status(iteration_best_hyp.hypothesis_id, "rejected")
|
|
no_improvement_count += 1
|
|
else:
|
|
# Walk-forward validation
|
|
if auto_mode or Confirm.ask("Run walk-forward validation?", default=True):
|
|
wfv = gate.run_walk_forward(iteration_best_params, cfg, store, builder,
|
|
runner, parser, log_rdr, analyzers, scorer)
|
|
console.print(f"WFV: OOS/IS ratio = {wfv.oos_is_ratio:.2f} "
|
|
f"(threshold {cfg['thresholds']['min_wfv_ratio']:.2f})")
|
|
if wfv.passed:
|
|
console.print(f"[green]Walk-forward PASSED[/green]")
|
|
else:
|
|
console.print(f"[yellow]Walk-forward FAILED — not promoting.[/yellow]")
|
|
store.update_hypothesis_status(iteration_best_hyp.hypothesis_id, "rejected")
|
|
no_improvement_count += 1
|
|
continue
|
|
|
|
# OOS test
|
|
run_oos = auto_mode or Confirm.ask("Run OOS validation?", default=False)
|
|
oos_score = None
|
|
if run_oos:
|
|
oos_metrics, _ = execute_run(
|
|
run_id=f"oos_{iteration}",
|
|
params=iteration_best_params,
|
|
period_start=per["oos_start"],
|
|
period_end=per["oos_end"],
|
|
phase="oos",
|
|
hypothesis_id=iteration_best_hyp.hypothesis_id,
|
|
cfg=cfg, store=store, builder=builder, runner=runner,
|
|
parser=parser, log_rdr=log_rdr, analyzers=analyzers, scorer=scorer,
|
|
)
|
|
if oos_metrics:
|
|
oos_score = oos_metrics.composite_score
|
|
oos_deg = (iteration_best.composite_score - oos_score) / max(0.001, iteration_best.composite_score)
|
|
if oos_deg > cfg["thresholds"]["max_oos_degradation"]:
|
|
console.print(f"[red]OOS degradation {oos_deg:.1%} > threshold. Rejected.[/red]")
|
|
store.update_hypothesis_status(iteration_best_hyp.hypothesis_id, "rejected")
|
|
no_improvement_count += 1
|
|
continue
|
|
display_metrics(oos_metrics, label="OOS Results")
|
|
|
|
# Promote candidate
|
|
candidate = Candidate(
|
|
run_id=iteration_best.run_id,
|
|
composite_score=iteration_best.composite_score,
|
|
oos_score=oos_score,
|
|
params=iteration_best_params,
|
|
)
|
|
store.save_candidate(candidate)
|
|
store.update_hypothesis_status(iteration_best_hyp.hypothesis_id, "validated")
|
|
console.print(f"[bold green]✅ Candidate C{candidate.candidate_id} promoted![/bold green]")
|
|
|
|
# Update baseline
|
|
improvement = iteration_best.composite_score - best_score
|
|
if improvement >= conv_thr:
|
|
current_params = iteration_best_params
|
|
current_metrics = iteration_best
|
|
best_score = iteration_best.composite_score
|
|
no_improvement_count = 0
|
|
else:
|
|
no_improvement_count += 1
|
|
|
|
# Convergence check
|
|
if no_improvement_count >= conv_win:
|
|
console.print(
|
|
f"[yellow]Convergence: no improvement in {no_improvement_count} iterations. Stopping.[/yellow]"
|
|
)
|
|
break
|
|
|
|
if not (auto_mode or Confirm.ask("Continue to next iteration?", default=True)):
|
|
break
|
|
|
|
# ── Summary ───────────────────────────────────────────────────────────────
|
|
console.rule("[bold]Optimization Complete[/bold]")
|
|
candidates = store.list_candidates()
|
|
if candidates:
|
|
console.print(f"[bold green]{len(candidates)} candidate(s) promoted.[/bold green]")
|
|
console.print(f"Best composite score: {max(c['composite_score'] for c in candidates):.4f}")
|
|
else:
|
|
console.print("[yellow]No candidates were promoted in this session.[/yellow]")
|
|
|
|
|
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
|
|
if __name__ == "__main__":
|
|
parser_cli = argparse.ArgumentParser(description="MT5 EA Strategy Optimizer")
|
|
parser_cli.add_argument("--auto", action="store_true", help="Run fully automated (no prompts)")
|
|
parser_cli.add_argument("--baseline", action="store_true", help="Run baseline + analysis only")
|
|
parser_cli.add_argument("--log-level", default="INFO", help="Logging level")
|
|
args = parser_cli.parse_args()
|
|
|
|
logger.remove()
|
|
logger.add(sys.stderr, level=args.log_level)
|
|
logger.add("optimizer.log", level="DEBUG", rotation="10 MB")
|
|
|
|
main(auto_mode=args.auto, baseline_only=args.baseline)
|