diff --git a/optimizer_loop.py b/optimizer_loop.py index d98717f..7a4e771 100644 --- a/optimizer_loop.py +++ b/optimizer_loop.py @@ -146,6 +146,7 @@ class OptimizerLoop: self.best_score = baseline_metrics.composite_score current_params = default_params.copy() current_metrics = baseline_metrics + current_findings = findings # reuse in iter 1, avoid double-emit no_improve_count = 0 max_iter = cfg["optimization"]["max_iterations"] @@ -168,11 +169,14 @@ class OptimizerLoop: if trades_df.empty and not baseline_trades.empty: trades_df = baseline_trades - # Analysis - self.phase = "analyze" - findings = self._run_analysis( - current_metrics.run_id, trades_df, current_metrics, analyzers, store - ) + # Analysis — reuse cached findings when re-analyzing same run_id + if current_findings is not None and trades_df.empty: + findings = current_findings + else: + findings = self._run_analysis( + current_metrics.run_id, trades_df, current_metrics, analyzers, store + ) + current_findings = None # only reuse once if not findings: self._emit("log", {"level": "warn", "msg": "No actionable findings. Stopping."}) @@ -251,6 +255,17 @@ class OptimizerLoop: store.update_hypothesis_status(hyp.hypothesis_id, "tested", run_id) self.session_tested_deltas.append(hyp.param_delta) # track for session dedup + # Emit score update for every run so chart populates + self._emit("score_update", { + "iteration": self.iteration, + "run_id": run_id, + "score": round(test_metrics.composite_score, 4), + "calmar": round(test_metrics.calmar_ratio, 4), + "pf": round(test_metrics.profit_factor, 4), + "ts": datetime.utcnow().isoformat(), + "promoted": False, + }) + if iteration_best is None or test_metrics.composite_score > iteration_best.composite_score: iteration_best = test_metrics iteration_best_params = test_params @@ -262,7 +277,7 @@ class OptimizerLoop: # Validation gate self.phase = "validate" - gate_result = gate.run_is_check(iteration_best) + gate_result = gate.run_is_check(iteration_best, baseline_score=current_metrics.composite_score) if not gate_result.passed: self._emit("log", {"level": "error", "msg": f"IS gate failed: {gate_result.reason}"}) store.update_hypothesis_status(iteration_best_hyp.hypothesis_id, "rejected") diff --git a/ui/static/js/dashboard.js b/ui/static/js/dashboard.js index 11dbb93..65b8cbb 100644 --- a/ui/static/js/dashboard.js +++ b/ui/static/js/dashboard.js @@ -258,9 +258,15 @@ function addCandidate(d) { function pushChartPoint(d) { const labels = scoreChart.data.labels; - labels.push(`Iter ${d.iteration}`); + // Label: 'Baseline' for first point, 'Iter N · run_id' for hypothesis runs + const lbl = d.run_id + ? (d.run_id.startsWith('baseline') ? 'Baseline' : `It${d.iteration}·${d.run_id.split('_').pop()}`) + : `Iter ${d.iteration}`; + labels.push(lbl); scoreChart.data.datasets[0].data.push(d.score); - scoreChart.data.datasets[1].data.push(d.calmar > 1 ? 1 : d.calmar / 4); // normalize calmar to 0-1 + // Normalize calmar: clamp -0.5..2.0 → 0..1 for display + const calmarNorm = Math.max(0, Math.min(1, (d.calmar + 0.5) / 2.5)); + scoreChart.data.datasets[1].data.push(calmarNorm); if (labels.length > 50) { labels.shift(); scoreChart.data.datasets.forEach(ds => ds.data.shift()); @@ -309,10 +315,14 @@ socket.on('run_started', d => { socket.on('run_complete', d => { updateMetrics(d); - const sign = d.score > 0 ? '✓' : '•'; + const sign = d.score > 0 ? '\u2713' : '\u2022'; addLog(d.score > 0.3 ? 'success' : 'info', `${sign} ${d.run_id}: Score=${d.score} | Calmar=${d.calmar} | PF=${d.profit_factor} | DD=${d.drawdown_pct}%` ); + // Push baseline run to chart immediately (hypothesis runs pushed via score_update) + if (d.run_id && d.run_id.startsWith('baseline')) { + pushChartPoint({ iteration: 0, run_id: d.run_id, score: d.score, calmar: d.calmar }); + } }); socket.on('run_failed', d => { @@ -333,7 +343,10 @@ socket.on('hypothesis_testing', d => { socket.on('score_update', d => { pushChartPoint(d); - document.getElementById('hdr-score').textContent = d.score.toFixed(4); + // Update best score header only when a candidate is actually promoted + if (d.promoted) { + document.getElementById('hdr-score').textContent = d.score.toFixed(4); + } }); socket.on('candidate_promoted', d => { diff --git a/ui/templates/reports_index.html b/ui/templates/reports_index.html index 0fc1d74..4b255ce 100644 --- a/ui/templates/reports_index.html +++ b/ui/templates/reports_index.html @@ -38,7 +38,7 @@