fix: dashboard goes silent during Phase 1 (no AI thinking, empty Param Changes panel)
Three issues caused the dashboard to look frozen during the long Phase 1 exploration phase, even when 2+ runs had completed: 1. AI Thinking feed only got ONE entry (the phase-start banner) and then went silent until Phase 2. Phase 1 is LHS sampling so there are no AI calls — but we can still narrate observations rule-based. Added _narrate_phase1_run() that emits a per-result message tuned to what actually happened: 'PF 1.8, DD 14% — strong region, worth refining' (success), '$-734 loss with 191 trades — skipping' (warning), 'only 3 trades — params too restrictive' (info), etc. Mediocre runs are throttled to 1-in-5 to avoid feed spam 2. Parameter Changes panel showed 'No iterations yet — AI-driven param edits appear per iteration' which is misleading during Phase 1 (where there are no iterations YET, by design). Empty-state message now phase-aware: shows 'Exploration phase — changes appear in Phase 2' during phase1, 'Waiting for first AI iteration' if autonomous, or 'Random-neighbor refinement — enable Autonomous AI for AI changes' if not autonomous 3. AI Summary still said 'Waiting for optimization to start...' even while the optimization was actively running. New refreshAIWaitingState() updates the message based on (a) whether a run is active and (b) whether the API key is set. When running with no key, shows 'AI insights are off — no Anthropic API key set' with a 'Configure API key in Settings →' link Frontend now also tracks state.phase and state.autonomous from phase_start events so all empty-state messages stay in sync.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 = '<div class="live-intel-empty">No iterations yet.<br><span style="font-size:0.68rem">AI-driven parameter edits appear per iteration.</span></div>';
|
||||
// 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 = `<div class="live-intel-empty">${escapeHtml(msg)}<br><span style="font-size:0.68rem">${escapeHtml(sub)}</span></div>`;
|
||||
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)
|
||||
|
||||
@@ -1969,10 +1969,14 @@
|
||||
<a href="#" class="view-details-link" id="ai-view-details" onclick="toggleAIDetails(); return false;">View Details →</a>
|
||||
</div>
|
||||
|
||||
<!-- Waiting state -->
|
||||
<!-- Waiting / no-key state -->
|
||||
<div class="ai-waiting" id="ai-waiting">
|
||||
<div class="ai-pulse-dot"></div>
|
||||
<span>Waiting for optimization to start...</span>
|
||||
<span id="ai-waiting-text">Waiting for optimization to start...</span>
|
||||
<a href="#" id="ai-waiting-action" onclick="openSettings(); return false;"
|
||||
style="display:none;font-size:0.72rem;color:var(--accent2);text-decoration:underline;cursor:pointer;">
|
||||
Configure API key in Settings →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Active state -->
|
||||
|
||||
Reference in New Issue
Block a user