Files
LEGSTECH Optimizer 6caafdb794 feat: open-source release — AI-driven autonomous optimization with live visibility
Major upgrade making the AI loop visible and the project ready for public release.

UI / UX
- Live AI Thinking Feed: streams reasoning, decisions, and outcomes per iteration
- Parameter Changes panel: prev → new + reason for every AI-driven edit
- Validation Activity panel: out-of-sample + sensitivity runs with live metrics
- Early Termination banner: surfaces why optimization stopped (targets met, no profit, budget, stuck, user stop)
- 3-phase tracker renamed Exploration / Iteration / Validation with live N/total
- Best Result modal exposes Evolution Path showing how the AI arrived at the winner
- Run-detail modal accessible from every recent run row
- Setup form validation (dates, walk-forward order, params selection, AI targets)
- Pause button removed; misleading sidebar nav consolidated to Dashboard / New Run / Reports / Source

Backend
- AIGuidedLoop streams ai_thinking, param_changes, ai_targets_met, ai_stuck
- Pipeline emits validation_start / validation_run_start / validation_run_complete / validation_done
- Pipeline emits early_termination on every early-stop path
- /api/best_result returns best run + full evolution chain
- /api/run/<id> + /api/runs sorted by ts
- AIReasoner falls back to ANTHROPIC_API_KEY env var when config is a placeholder
- Demo mode (APEX_DEMO_MODE=1) generates deterministic synthetic backtests so judges can run end-to-end without MT5

Open-source readiness
- README.md with pitch, demo flow, architecture diagram, quickstart, event reference
- LICENSE (MIT)
- config.example.yaml template (config.yaml now git-ignored)
- requirements.txt: added anthropic / requests / psutil / beautifulsoup4, capped majors
- .gitignore: secrets, *.set, scratch screenshots, ea_registry.yaml
- demo/run_demo.py: one-command offline demo runner
- 10 polished screenshots for README + judge review
2026-04-25 11:39:17 +00:00

138 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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
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
# ── Autonomous AI Loop settings ───────────────────────────────────────────
autonomous_mode: bool = False # Replace Phase 2 with AI-guided loop
autonomous_max_iterations: int = 10 # Max AI-directed iterations
target_profit_factor: float = 1.5 # Stop when PF ≥ this
target_max_drawdown_pct: float = 20.0 # Stop when DD ≤ this %
target_min_calmar: float = 0.5 # Stop when Calmar ≥ this
# ── 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 1050
@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:
phase2 = self.autonomous_max_iterations if self.autonomous_mode else self.phase2_samples
return self.phase1_samples + phase2 + 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
def f(key, default=0.0):
try: return float(form.get(key, default))
except (ValueError, TypeError): return default
def b(key):
v = form.get(key, False)
if isinstance(v, bool): return v
return str(v).lower() in ("true", "1", "yes", "on")
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", []),
# Autonomous loop
autonomous_mode = b("autonomous_mode"),
autonomous_max_iterations = i("autonomous_max_iterations", 10),
target_profit_factor = f("target_profit_factor", 1.5),
target_max_drawdown_pct = f("target_max_drawdown_pct", 20.0),
target_min_calmar = f("target_min_calmar", 0.5),
)
cfg.derive_samples()
return cfg