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:
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
optimizer/session_config.py
|
||||
Holds all user choices for one optimization session.
|
||||
Passed from the /setup form → /api/start → pipeline.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import Literal, Optional
|
||||
|
||||
|
||||
ObjectiveType = Literal["balanced", "max_profit", "min_drawdown"]
|
||||
BudgetMinutes = Literal[30, 60, 120]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SessionConfig:
|
||||
"""Everything the user configures on the /setup page."""
|
||||
|
||||
# EA identity
|
||||
ea_name: str = "LEGSTECH_EA_V2"
|
||||
symbol: str = "XAUUSD"
|
||||
timeframe: str = "H1"
|
||||
|
||||
# Training period (in-sample)
|
||||
train_start: str = "2022.01.01"
|
||||
train_end: str = "2023.12.31"
|
||||
|
||||
# Validation period (out-of-sample)
|
||||
val_start: str = "2024.01.01"
|
||||
val_end: str = "2024.06.30"
|
||||
|
||||
# Optimization objective
|
||||
objective: ObjectiveType = "balanced"
|
||||
|
||||
# Time budget
|
||||
budget_minutes: int = 60 # 30 | 60 | 120
|
||||
|
||||
# Advanced: which params the user wants to optimize
|
||||
# Empty list means "use profile's default optimize_params"
|
||||
selected_params: list[str] = field(default_factory=list)
|
||||
|
||||
# Phase 1 sample count (derived from budget, not user-set directly)
|
||||
phase1_samples: int = 20
|
||||
|
||||
# ── Derived helpers ───────────────────────────────────────────────────────
|
||||
|
||||
def derive_samples(self, seconds_per_run: float = 75.0) -> None:
|
||||
"""
|
||||
Automatically set phase1_samples based on time budget.
|
||||
Reserves ~40% of budget for Phase 2 + Phase 3.
|
||||
"""
|
||||
total_seconds = self.budget_minutes * 60
|
||||
phase1_budget = total_seconds * 0.55 # 55% for broad search
|
||||
n = int(phase1_budget / seconds_per_run)
|
||||
self.phase1_samples = max(10, min(n, 50)) # clamp 10–50
|
||||
|
||||
@property
|
||||
def phase2_samples(self) -> int:
|
||||
"""Refinement runs: top 3 configs × 3 neighbors each = 9."""
|
||||
return 9
|
||||
|
||||
@property
|
||||
def phase3_samples(self) -> int:
|
||||
"""Validation: 2 OOS runs + 3 sensitivity = 5."""
|
||||
return 5
|
||||
|
||||
@property
|
||||
def total_budget_runs(self) -> int:
|
||||
return self.phase1_samples + self.phase2_samples + self.phase3_samples
|
||||
|
||||
# ── Scoring weights based on objective ───────────────────────────────────
|
||||
|
||||
@property
|
||||
def scoring_weights(self) -> dict:
|
||||
if self.objective == "max_profit":
|
||||
return {"calmar": 0.3, "profit_factor": 0.3, "win_rate": 0.2, "net_profit": 0.2}
|
||||
if self.objective == "min_drawdown":
|
||||
return {"calmar": 0.6, "profit_factor": 0.25, "win_rate": 0.15, "net_profit": 0.0}
|
||||
# balanced (default)
|
||||
return {"calmar": 0.5, "profit_factor": 0.3, "win_rate": 0.2, "net_profit": 0.0}
|
||||
|
||||
# ── Serialization ─────────────────────────────────────────────────────────
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return asdict(self)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d: dict) -> "SessionConfig":
|
||||
known = {k: v for k, v in d.items() if k in cls.__dataclass_fields__}
|
||||
return cls(**known)
|
||||
|
||||
@classmethod
|
||||
def from_form(cls, form: dict) -> "SessionConfig":
|
||||
"""
|
||||
Parse raw form POST data (all values are strings).
|
||||
Handles type coercion, validation, and defaults.
|
||||
"""
|
||||
def s(key, default=""): return str(form.get(key, default)).strip()
|
||||
def i(key, default=0):
|
||||
try: return int(form.get(key, default))
|
||||
except (ValueError, TypeError): return default
|
||||
|
||||
cfg = cls(
|
||||
ea_name = s("ea_name", "LEGSTECH_EA_V2"),
|
||||
symbol = s("symbol", "XAUUSD").upper(),
|
||||
timeframe = s("timeframe", "H1").upper(),
|
||||
train_start = s("train_start", "2022.01.01").replace("-", "."),
|
||||
train_end = s("train_end", "2023.12.31").replace("-", "."),
|
||||
val_start = s("val_start", "2024.01.01").replace("-", "."),
|
||||
val_end = s("val_end", "2024.06.30").replace("-", "."),
|
||||
objective = s("objective", "balanced"),
|
||||
budget_minutes = i("budget_minutes", 60),
|
||||
selected_params = form.get("selected_params", []),
|
||||
)
|
||||
cfg.derive_samples()
|
||||
return cfg
|
||||
Reference in New Issue
Block a user