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,76 @@
|
||||
"""
|
||||
optimizer/budget.py
|
||||
Time budget manager — tracks elapsed time and estimates remaining runs.
|
||||
Updates its average-run-time estimate after every completed run.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class BudgetManager:
|
||||
"""
|
||||
Tracks the time budget for an optimization session.
|
||||
|
||||
Usage:
|
||||
budget = BudgetManager(budget_minutes=60)
|
||||
budget.start()
|
||||
...after each run:
|
||||
budget.record_run(elapsed_seconds)
|
||||
if budget.is_exhausted():
|
||||
break
|
||||
remaining = budget.estimated_runs_remaining()
|
||||
"""
|
||||
|
||||
def __init__(self, budget_minutes: int, initial_seconds_per_run: float = 75.0):
|
||||
self.budget_seconds = budget_minutes * 60
|
||||
self.avg_seconds_per_run = initial_seconds_per_run
|
||||
self._start_ts: float = None
|
||||
self._run_times: list[float] = []
|
||||
|
||||
def start(self) -> None:
|
||||
self._start_ts = time.time()
|
||||
logger.info(f"BudgetManager: started. Budget = {self.budget_seconds/60:.0f} min")
|
||||
|
||||
@property
|
||||
def elapsed_seconds(self) -> float:
|
||||
if self._start_ts is None:
|
||||
return 0.0
|
||||
return time.time() - self._start_ts
|
||||
|
||||
@property
|
||||
def remaining_seconds(self) -> float:
|
||||
return max(0.0, self.budget_seconds - self.elapsed_seconds)
|
||||
|
||||
@property
|
||||
def elapsed_pct(self) -> float:
|
||||
return min(100.0, self.elapsed_seconds / self.budget_seconds * 100)
|
||||
|
||||
def record_run(self, seconds: float) -> None:
|
||||
"""Call after each run completes. Updates rolling average."""
|
||||
self._run_times.append(seconds)
|
||||
# Rolling average (last 5 runs for responsiveness)
|
||||
recent = self._run_times[-5:]
|
||||
self.avg_seconds_per_run = sum(recent) / len(recent)
|
||||
|
||||
def estimated_runs_remaining(self) -> int:
|
||||
"""How many more runs fit in the remaining budget."""
|
||||
if self.avg_seconds_per_run <= 0:
|
||||
return 0
|
||||
return max(0, int(self.remaining_seconds / self.avg_seconds_per_run))
|
||||
|
||||
def is_exhausted(self) -> bool:
|
||||
"""True when less than 1 average run's time remains."""
|
||||
return self.remaining_seconds < self.avg_seconds_per_run
|
||||
|
||||
def can_fit(self, n_runs: int) -> bool:
|
||||
"""True if n_runs more runs fit in remaining budget."""
|
||||
return self.remaining_seconds >= n_runs * self.avg_seconds_per_run
|
||||
|
||||
def summary(self) -> str:
|
||||
return (
|
||||
f"Budget: {self.elapsed_seconds/60:.1f}/{self.budget_seconds/60:.0f} min used. "
|
||||
f"Avg run: {self.avg_seconds_per_run:.0f}s. "
|
||||
f"Est. remaining: {self.estimated_runs_remaining()} runs."
|
||||
)
|
||||
Reference in New Issue
Block a user