feat: Smart Autonomous 3-Phase Optimizer — complete redesign
PROBLEM: Old system repeated identical params, all scores flat at 0.2500,
user had zero control over symbol/TF/dates. Not smart, not dynamic.
NEW ARCHITECTURE:
optimizer/ (NEW package)
├── __init__.py
├── session_config.py User choices (EA, symbol, TF, dates, budget, objective)
├── lhs_sampler.py Latin Hypercube Sampling — diverse exploration
├── result_ranker.py Relative scoring (best in session=1.0, worst=0.0)
├── budget.py Time budget tracker
└── pipeline.py 3-phase orchestrator
Phase 1 — Broad Discovery (LHS, 20-26 runs):
Samples FULL parameter space, not just defaults±tiny step
LHS guarantees coverage: all 17 optimizable LEGSTECH params explored
Relative ranking: profitable configs float top, losers score 0
Phase 2 — Refinement (9 runs):
Neighbor search around top 3 configs at ±20% range (not ±0.5 step)
Keeps best of Phase1 vs Phase2 — never regresses
Phase 3 — Validation (5 runs):
OOS backtest on unseen data period
Sensitivity test: nudge params ±20%, detect fragility
Verdict: RECOMMENDED / RISKY / NOT_RELIABLE
Output: Clean downloadable .set file via /download_set/<run_id>
UI REDESIGN:
ui/templates/landing.html New / homepage (was old dashboard)
ui/templates/setup.html New /setup — EA, symbol, TF, dates, budget, objective
ui/templates/dashboard.html Updated /dashboard with:
- 5-step phase indicator
- Real progress bar per run
- Phase 1 results table (top 5 after phase1)
- Verdict banner with download button
- No-profitable-config warning
ui/static/js/dashboard.js Handles 8 new pipeline SocketIO events
app.py New routes: /, /setup, /dashboard
/api/start accepts full SessionConfig JSON
/download_set/<id> serves optimized .set
ea/registry.py +list_all() for setup page dropdown
ui/templates/reports_index.html Back to Dashboard → /dashboard (was /)
ui/static/css/style.css +dot-warn, dot-done, profit-pos/neg, aliases
FIXES:
Score no longer flat 0.2500 (was: absolute thresholds on losing EA)
User now controls: symbol, timeframe, dates, budget, objective
Parameters now span full range (was: tiny step from defaults)
Verdict is actionable: RECOMMENDED / RISKY / NOT_RELIABLE with reason
TESTED:
8/8 pre-flight checks pass
Browser test: landing ✓, setup form ✓, /dashboard ✓,
phase indicator active ✓, /reports ✓, back link ✓
This commit is contained in:
@@ -1,18 +1,22 @@
|
||||
"""
|
||||
app.py — MT5 EA Optimizer Web App
|
||||
Double-click to launch. Browser opens automatically at http://localhost:5000
|
||||
app.py — MT5 Smart EA Optimizer Web App
|
||||
Routes:
|
||||
/ → Landing page
|
||||
/setup → Configure new optimization session
|
||||
/dashboard → Live optimization dashboard
|
||||
/reports → Past runs browser
|
||||
"""
|
||||
import sys, os, threading, webbrowser, time
|
||||
from pathlib import Path
|
||||
|
||||
# ── Make sure imports resolve from project root ───────────────────────────────
|
||||
BASE_DIR = Path(__file__).parent
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
from flask import Flask, render_template, jsonify, request, send_from_directory
|
||||
from flask import Flask, render_template, jsonify, request, send_from_directory, redirect
|
||||
from flask_socketio import SocketIO, emit
|
||||
|
||||
from optimizer_loop import OptimizerLoop
|
||||
from optimizer.pipeline import OptimizationPipeline
|
||||
from optimizer.session_config import SessionConfig
|
||||
|
||||
# ── App setup ─────────────────────────────────────────────────────────────────
|
||||
app = Flask(__name__,
|
||||
@@ -24,76 +28,151 @@ socketio = SocketIO(app, cors_allowed_origins="*", async_mode="threading")
|
||||
REPORTS_DIR = BASE_DIR / "Reports"
|
||||
REPORTS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
# Global optimizer instance
|
||||
optimizer: OptimizerLoop = None
|
||||
optimizer_thread: threading.Thread = None
|
||||
# Global pipeline instance
|
||||
pipeline: OptimizationPipeline = None
|
||||
pipeline_thread: threading.Thread = None
|
||||
|
||||
|
||||
# ── Routes ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
def landing():
|
||||
return render_template("landing.html")
|
||||
|
||||
|
||||
@app.route("/setup")
|
||||
def setup():
|
||||
"""Setup page: EA selector, dates, budget, objective."""
|
||||
import yaml
|
||||
from ea.registry import EARegistry
|
||||
try:
|
||||
reg = EARegistry(str(BASE_DIR / "config.yaml"))
|
||||
eas = reg.list_all()
|
||||
default_ea = eas[0].name if eas else "LEGSTECH_EA_V2"
|
||||
|
||||
# Load param list for the first EA (or selected)
|
||||
ea_name = request.args.get("ea", default_ea)
|
||||
profile = reg.get(ea_name)
|
||||
schema = reg.get_schema(profile, apply_optimize_selection=False)
|
||||
params = [p for p in schema.all_params() if p.type != "fixed"]
|
||||
except Exception as e:
|
||||
eas = []
|
||||
default_ea = "LEGSTECH_EA_V2"
|
||||
params = []
|
||||
|
||||
return render_template("setup.html",
|
||||
registered_eas=eas,
|
||||
default_ea=default_ea,
|
||||
params=params)
|
||||
|
||||
|
||||
@app.route("/dashboard")
|
||||
def dashboard():
|
||||
return render_template("dashboard.html")
|
||||
|
||||
|
||||
# Keep old / redirect for muscle memory
|
||||
@app.route("/index")
|
||||
def old_index():
|
||||
return redirect("/dashboard")
|
||||
|
||||
|
||||
# ── API ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route("/api/status")
|
||||
def status():
|
||||
if optimizer is None:
|
||||
return jsonify({"state": "idle", "iteration": 0, "best_score": 0})
|
||||
return jsonify(optimizer.get_status())
|
||||
if pipeline is None:
|
||||
return jsonify({"state": "idle", "run_count": 0, "total_runs": 0,
|
||||
"best_score": 0, "phase": "idle"})
|
||||
return jsonify(pipeline.get_status())
|
||||
|
||||
|
||||
@app.route("/api/start", methods=["POST"])
|
||||
def start():
|
||||
global optimizer, optimizer_thread
|
||||
if optimizer and optimizer.running:
|
||||
return jsonify({"ok": False, "msg": "Already running"})
|
||||
|
||||
global pipeline, pipeline_thread
|
||||
|
||||
if pipeline and pipeline.running:
|
||||
return jsonify({"ok": False, "msg": "Optimization already running"})
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
optimizer = OptimizerLoop(
|
||||
|
||||
try:
|
||||
session = SessionConfig.from_dict(data)
|
||||
session.derive_samples()
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"Invalid config: {e}"})
|
||||
|
||||
pipeline = OptimizationPipeline(
|
||||
config_path=str(BASE_DIR / "config.yaml"),
|
||||
socketio=socketio,
|
||||
reports_dir=REPORTS_DIR,
|
||||
auto_mode=data.get("auto", True),
|
||||
)
|
||||
optimizer_thread = threading.Thread(target=optimizer.run, daemon=True)
|
||||
optimizer_thread.start()
|
||||
return jsonify({"ok": True})
|
||||
pipeline.configure(session)
|
||||
|
||||
pipeline_thread = threading.Thread(target=pipeline.run, daemon=True)
|
||||
pipeline_thread.start()
|
||||
|
||||
@app.route("/api/pause", methods=["POST"])
|
||||
def pause():
|
||||
if optimizer:
|
||||
optimizer.toggle_pause()
|
||||
return jsonify({"ok": True, "paused": optimizer.paused})
|
||||
return jsonify({"ok": False})
|
||||
return jsonify({"ok": True, "total_runs": session.total_budget_runs})
|
||||
|
||||
|
||||
@app.route("/api/stop", methods=["POST"])
|
||||
def stop():
|
||||
if optimizer:
|
||||
optimizer.stop()
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.route("/api/skip", methods=["POST"])
|
||||
def skip():
|
||||
if optimizer:
|
||||
optimizer.skip_hypothesis()
|
||||
if pipeline:
|
||||
pipeline.stop()
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
@app.route("/api/history")
|
||||
def history():
|
||||
if optimizer is None:
|
||||
"""Score history for chart — built from pipeline results."""
|
||||
if pipeline is None:
|
||||
return jsonify([])
|
||||
return jsonify(optimizer.score_history)
|
||||
results = pipeline.phase1_results + pipeline.phase2_results
|
||||
return jsonify([
|
||||
{
|
||||
"run_id": r.run_id,
|
||||
"score": round(r.score, 4),
|
||||
"calmar": round(r.calmar, 3),
|
||||
"passing": r.passing,
|
||||
"phase": r.phase,
|
||||
}
|
||||
for r in results
|
||||
])
|
||||
|
||||
|
||||
@app.route("/api/ea_params")
|
||||
def ea_params():
|
||||
"""Return param list for a given EA (used by setup page AJAX)."""
|
||||
ea_name = request.args.get("ea", "")
|
||||
try:
|
||||
from ea.registry import EARegistry
|
||||
reg = EARegistry(str(BASE_DIR / "config.yaml"))
|
||||
profile = reg.get(ea_name)
|
||||
schema = reg.get_schema(profile, apply_optimize_selection=False)
|
||||
return jsonify([
|
||||
{"name": p.name, "type": p.type,
|
||||
"range": p.range_label, "optimize": p.optimize}
|
||||
for p in schema.all_params() if p.type != "fixed"
|
||||
])
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/download_set/<run_id>")
|
||||
def download_set(run_id):
|
||||
"""Serve the optimized .set file for download."""
|
||||
run_dir = REPORTS_DIR / run_id
|
||||
set_files = list(run_dir.glob("*.set")) if run_dir.exists() else []
|
||||
if not set_files:
|
||||
return "No .set file found", 404
|
||||
return send_from_directory(run_dir, set_files[0].name, as_attachment=True)
|
||||
|
||||
|
||||
# ── Reports routes (unchanged) ────────────────────────────────────────────────
|
||||
|
||||
@app.route("/reports")
|
||||
@app.route("/reports/")
|
||||
def reports_index():
|
||||
"""Reports browser page — fixes the 404 on the Reports button."""
|
||||
import json, re
|
||||
runs = []
|
||||
for run_dir in sorted(REPORTS_DIR.iterdir(), reverse=True) if REPORTS_DIR.exists() else []:
|
||||
@@ -102,12 +181,10 @@ def reports_index():
|
||||
summary = run_dir / "summary.json"
|
||||
if summary.exists():
|
||||
try:
|
||||
txt = summary.read_text(encoding="utf-8")
|
||||
# Fix legacy NaN values (invalid JSON) written before the fix
|
||||
txt = re.sub(r'\bNaN\b', 'null', txt)
|
||||
txt = re.sub(r'\bInfinity\b', 'null', txt)
|
||||
txt = summary.read_text(encoding="utf-8")
|
||||
txt = re.sub(r'\bNaN\b', 'null', txt)
|
||||
txt = re.sub(r'\bInfinity\b', 'null', txt)
|
||||
data = json.loads(txt)
|
||||
# Replace None scores with 0 for display
|
||||
data["score"] = data.get("score") or 0
|
||||
data["score_delta"] = data.get("score_delta") or 0
|
||||
runs.append(data)
|
||||
@@ -118,7 +195,6 @@ def reports_index():
|
||||
|
||||
@app.route("/reports/<path:filename>")
|
||||
def reports_file(filename):
|
||||
"""Serve individual report files (HTML, CSV, JSON)."""
|
||||
return send_from_directory(REPORTS_DIR, filename)
|
||||
|
||||
|
||||
@@ -133,9 +209,9 @@ def runs_list():
|
||||
summary = run_dir / "summary.json"
|
||||
if summary.exists():
|
||||
try:
|
||||
txt = summary.read_text(encoding="utf-8")
|
||||
txt = re.sub(r'\bNaN\b', 'null', txt)
|
||||
txt = re.sub(r'\bInfinity\b', 'null', txt)
|
||||
txt = summary.read_text(encoding="utf-8")
|
||||
txt = re.sub(r'\bNaN\b', 'null', txt)
|
||||
txt = re.sub(r'\bInfinity\b', 'null', txt)
|
||||
data = json.loads(txt)
|
||||
data["score"] = data.get("score") or 0
|
||||
data["score_delta"] = data.get("score_delta") or 0
|
||||
@@ -145,12 +221,12 @@ def runs_list():
|
||||
return jsonify(runs[:50])
|
||||
|
||||
|
||||
# ── SocketIO events ───────────────────────────────────────────────────────────
|
||||
# ── SocketIO ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@socketio.on("connect")
|
||||
def on_connect():
|
||||
if optimizer:
|
||||
emit("status_sync", optimizer.get_status())
|
||||
if pipeline:
|
||||
emit("status_sync", pipeline.get_status())
|
||||
|
||||
|
||||
# ── Launch ────────────────────────────────────────────────────────────────────
|
||||
@@ -162,7 +238,7 @@ def open_browser():
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print(" MT5 EA Optimizer — Starting...")
|
||||
print(" MT5 Smart EA Optimizer — Starting...")
|
||||
print(" Opening browser at http://localhost:5000")
|
||||
print("=" * 60)
|
||||
threading.Thread(target=open_browser, daemon=True).start()
|
||||
|
||||
Reference in New Issue
Block a user