diff --git a/optimizer/pipeline.py b/optimizer/pipeline.py index 859cb8a..f8d5d7e 100644 --- a/optimizer/pipeline.py +++ b/optimizer/pipeline.py @@ -312,6 +312,10 @@ class OptimizationPipeline: "budget_summary": budget.summary(), }) + # Narrate every Phase 1 outcome so the AI Thinking feed stays alive + # even though no API call is made — this is observation, not AI reasoning + self._narrate_phase1_run(i + 1, cfg.phase1_samples, result) + if budget.is_exhausted(): self._log("warning", "⏱ Time budget reached during Phase 1") break @@ -1126,6 +1130,66 @@ class OptimizationPipeline: self._thinking_log = self._thinking_log[-200:] self._emit("ai_thinking", payload) + def _narrate_phase1_run(self, idx: int, total: int, result: RankedResult) -> None: + """ + Emit a short observation per Phase 1 run so the AI Thinking feed + stays alive during the LHS exploration phase. No API call — pure + rule-based narration of what just happened. + + Cadence: + * Every passing result → success message + * Every clearly bad result → warning + * Once every 5 mediocre runs → progress beat + """ + try: + pf = float(getattr(result, "profit_factor", 0) or 0) + dd = float(getattr(result, "max_drawdown", 0) or 0) # fraction + tr = int(getattr(result, "total_trades", 0) or 0) + np_ = float(getattr(result, "net_profit", 0) or 0) + passing = bool(getattr(result, "passing", False)) + + tag = f"Run {idx}/{total}" + + if tr == 0 or tr < 10: + msg = ( + f"{tag}: only {tr} trades — params likely too restrictive " + f"(session window, signal threshold, or risk filter blocking entries)." + ) + kind = "info" + elif passing and pf >= 1.5 and dd * 100 <= 15: + msg = ( + f"{tag}: PF {pf:.2f}, DD {dd*100:.1f}%, {tr} trades — " + f"strong region, worth refining." + ) + kind = "success" + elif passing: + msg = ( + f"{tag}: PF {pf:.2f}, DD {dd*100:.1f}% — passes gates but " + f"not a standout. Logged." + ) + kind = "info" + elif np_ < 0: + msg = ( + f"{tag}: ${np_:.0f} loss with {tr} trades. Skipping this region." + ) + kind = "warning" + elif dd * 100 > 25: + msg = ( + f"{tag}: drawdown {dd*100:.1f}% too high — EA over-leveraged " + f"in this parameter region." + ) + kind = "warning" + else: + # Mediocre — only emit one in five so we don't spam + if idx % 5 != 0: + return + msg = f"{tag}: PF {pf:.2f}, DD {dd*100:.1f}% — mediocre, moving on." + kind = "info" + + self._emit_thinking(msg, kind=kind) + except Exception as e: + logger.debug(f"_narrate_phase1_run failed: {e}") + def _emit_early_termination(self, reason_code: str, message: str, details: dict = None) -> None: """ Surface an early stop to the user. diff --git a/ui/static/js/dashboard.js b/ui/static/js/dashboard.js index d2fbb55..896440a 100644 --- a/ui/static/js/dashboard.js +++ b/ui/static/js/dashboard.js @@ -62,6 +62,9 @@ const state = { elapsedTimer: null, logCount: 0, bestProfit: null, + phase: 'idle', // current pipeline phase, lowercase + autonomous: false, // whether the current run uses the AI loop + aiKeySet: false, // whether ANTHROPIC_API_KEY is present sparkData: { profit: [], dd: [], pf: [], trades: [], runs: [] }, }; @@ -547,11 +550,37 @@ function setRunningState(isRunning, label) { if (isRunning) { if (label) safe('run-indicator-label', e => { e.textContent = label; }); startElapsedTimer(); + refreshAIWaitingState(); } else { stopElapsedTimer(); } } +/** + * Update the AI Summary panel's waiting message + action link based on + * whether (a) a run is active and (b) the API key is configured. + * Called whenever phase/state/aiKeySet changes. + */ +function refreshAIWaitingState() { + const waiting = el('ai-waiting'); + const txt = el('ai-waiting-text'); + const action = el('ai-waiting-action'); + if (!waiting || !txt) return; + // Don't override if AI content has already loaded + const content = el('ai-content'); + if (content && content.style.display !== 'none') return; + if (state.isRunning && !state.aiKeySet) { + txt.textContent = 'AI insights are off — no Anthropic API key set.'; + if (action) action.style.display = ''; + } else if (state.isRunning && state.aiKeySet) { + txt.textContent = 'Optimization running — first AI analysis fires after the next backtest.'; + if (action) action.style.display = 'none'; + } else { + txt.textContent = 'Waiting for optimization to start...'; + if (action) action.style.display = 'none'; + } +} + /* ══════════════════════════════════════════════════════════════════════ PHASE STEPS ══════════════════════════════════════════════════════════════════════ */ @@ -837,7 +866,11 @@ socket.on('run_complete', (d) => { socket.on('phase_start', (d) => { const phaseNum = phaseMap[d.phase] || parseInt(d.phase_num || d.phase || '1', 10); + state.phase = (d.phase || '').toLowerCase(); + if (d.mode === 'autonomous') state.autonomous = true; setPhaseActive(phaseNum, { total: d.total, mode: d.mode }); + // Re-render empty-state cards so they pick up the phase-aware message + renderParamChanges(); const label = d.label || ({ 1: 'Exploration', 2: (d.mode === 'autonomous' ? 'AI Iteration' : 'Iteration'), 3: 'Validation' }[phaseNum]) || ('Phase ' + phaseNum); @@ -1074,7 +1107,25 @@ function renderParamChanges() { const box = el('param-changes-list'); if (!box) return; if (!paramChangesState.iterations.length) { - box.innerHTML = '