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 = '
No iterations yet.
AI-driven parameter edits appear per iteration.
'; + // Tailor the empty-state message to the current phase + mode + const phase = (state.phase || '').toLowerCase(); + let msg, sub; + if (phase === 'phase1') { + msg = 'Exploration phase'; + sub = 'Parameter changes appear once Phase 2 (AI iteration) starts.'; + } else if (phase.startsWith('phase2')) { + msg = state.autonomous ? 'Waiting for first AI iteration…' : 'Random-neighbor refinement'; + sub = state.autonomous + ? 'Claude will choose the next parameter set after the first run finishes.' + : 'Enable Autonomous AI mode in setup to see AI-driven parameter changes.'; + } else if (phase.startsWith('phase3')) { + msg = 'Validation phase'; + sub = 'Phase 2 complete. See validation panel for OOS + sensitivity results.'; + } else { + msg = 'No iterations yet'; + sub = 'AI-driven parameter edits appear per iteration during Phase 2.'; + } + box.innerHTML = `
${escapeHtml(msg)}
${escapeHtml(sub)}
`; return; } // Newest first @@ -1381,12 +1432,20 @@ safe('btn-stop', stopBtn => { async function restoreHistory() { try { - const [histResp, aiResp, liveResp] = await Promise.all([ + const [histResp, aiResp, liveResp, settingsResp] = await Promise.all([ fetch('/api/history'), fetch('/api/ai_insight/latest'), fetch('/api/live_activity'), + fetch('/api/settings'), ]); + // Note whether the API key is set so the AI panel can show a useful empty state + try { + const s = await settingsResp.json(); + state.aiKeySet = !!(s && s.ai && s.ai.anthropic_api_key_set); + } catch (_) {} + refreshAIWaitingState(); + const runs = await histResp.json(); if (Array.isArray(runs) && runs.length) { // Populate recent runs table (last 8, newest first) diff --git a/ui/templates/dashboard.html b/ui/templates/dashboard.html index a0ae178..277867c 100644 --- a/ui/templates/dashboard.html +++ b/ui/templates/dashboard.html @@ -1969,10 +1969,14 @@ View Details → - +
- Waiting for optimization to start... + Waiting for optimization to start... +