document.addEventListener('DOMContentLoaded', () => { // --- Elements --- const btnScanUS = document.getElementById('btnScanUS'); const btnScanIN = document.getElementById('btnScanIN'); const signalsTableBody = document.getElementById('signalsTableBody'); const lastUpdated = document.getElementById('lastUpdated'); const pipelineFunnel = document.getElementById('pipelineFunnel'); const pipelineDetails = document.getElementById('pipelineDetails'); const regimeBanner = document.getElementById('regimeBanner'); const scanProgress = document.getElementById('scanProgress'); const progressStep = document.getElementById('progressStep'); const progressBar = document.getElementById('progressBar'); // Stats const statUniverse = document.getElementById('statUniverse'); const statRaw = document.getElementById('statRaw'); const statFiltered = document.getElementById('statFiltered'); const statFinal = document.getElementById('statFinal'); const statDuration = document.getElementById('statDuration'); // Chat const chatFab = document.getElementById('chatFab'); const chatPanel = document.getElementById('chatPanel'); const chatClose = document.getElementById('chatClose'); const chatInput = document.getElementById('chatInput'); const chatSend = document.getElementById('chatSend'); const chatMessages = document.getElementById('chatMessages'); const API_BASE = '/api'; const fmt = (n) => n != null ? Number(n).toLocaleString() : '0'; const fmtPrice = (n) => n ? parseFloat(n).toFixed(2) : '-'; // --- Pipeline Rendering --- const renderPipeline = (stages) => { if (!stages || stages.length === 0) { pipelineFunnel.innerHTML = `
Run a scan to see the filtering pipeline
`; return; } let html = ''; stages.forEach((stage, i) => { const stateClass = stage.count_out === 0 ? 'empty' : (i === stages.length - 1 ? 'active' : 'completed'); const timeStr = stage.duration_ms > 0 ? `${(stage.duration_ms / 1000).toFixed(1)}s` : ''; html += `
${stage.label}
${stage.count_out}
${timeStr ? `
${timeStr}
` : ''}
`; if (i < stages.length - 1) { const dropPct = stage.count_in > 0 ? Math.round((1 - stage.count_out / stage.count_in) * 100) : 0; html += `
`; } }); pipelineFunnel.innerHTML = html; }; // Stage details panel let _pipelineStages = []; window._showStageDetails = (idx) => { const stage = _pipelineStages[idx]; if (!stage) return; // Toggle active class document.querySelectorAll('.pipeline-stage').forEach((el, i) => { el.classList.toggle('active', i === idx); }); const dropCount = stage.count_in - stage.count_out; const dropPct = stage.count_in > 0 ? ((dropCount / stage.count_in) * 100).toFixed(1) : 0; let tickerHtml = ''; if (stage.tickers && stage.tickers.length > 0) { tickerHtml = '
' + stage.tickers.slice(0, 30).map(t => `${t}`).join('') + (stage.tickers.length > 30 ? `+${stage.tickers.length - 30} more` : '') + '
'; } let rejectedHtml = ''; if (stage.rejected && stage.rejected.length > 0) { rejectedHtml = '
Rejected:
' + stage.rejected.slice(0, 10).map(r => `
${r.ticker}${r.reason || r.signal_type || ''}
` ).join('') + (stage.rejected.length > 10 ? `
+${stage.rejected.length - 10} more
` : '') + '
'; } pipelineDetails.innerHTML = `
${stage.label}
In: ${stage.count_in}
Out: ${stage.count_out}
${dropCount > 0 ? `
Dropped: ${dropCount} (${dropPct}%)
` : ''} ${stage.duration_ms > 0 ? `
${(stage.duration_ms / 1000).toFixed(1)}s
` : ''}
${stage.details}
${tickerHtml} ${rejectedHtml} `; pipelineDetails.classList.add('visible'); }; // --- Regime Banner --- const renderRegimes = (regimes) => { if (!regimes || Object.keys(regimes).length === 0) { regimeBanner.style.display = 'none'; return; } regimeBanner.style.display = 'flex'; let html = ''; for (const [mkt, r] of Object.entries(regimes)) { const trendClass = r.trend === 'bullish' ? 'bullish' : (r.trend === 'bearish' ? 'bearish' : 'neutral'); const trendIcon = r.trend === 'bullish' ? 'fa-arrow-trend-up' : (r.trend === 'bearish' ? 'fa-arrow-trend-down' : 'fa-arrows-left-right'); html += `
${mkt.toUpperCase()} Market Regime
${(r.regime || '').replace(/_/g, ' ').toUpperCase()}
Trend: ${r.trend} | Volatility: ${r.volatility_state} | VIX: ${r.vix || 'N/A'} | Breadth: ${r.breadth || 'N/A'}
${r.description || ''}
`; } regimeBanner.innerHTML = html; }; // --- Stats --- const updateStats = (stats) => { if (!stats) return; statUniverse.innerText = fmt(stats.universe_size); statRaw.innerText = fmt(stats.raw_signals); statFiltered.innerText = fmt(stats.risk_filtered || stats.news_filtered || stats.regime_filtered || stats.raw_signals); statFinal.innerText = fmt(stats.final_signals); statDuration.innerText = stats.duration_seconds ? `${stats.duration_seconds}s` : '-'; }; // --- Signals Table --- const updateTable = (signals) => { if (!signals || signals.length === 0) { signalsTableBody.innerHTML = `No opportunities found in the last scan.`; return; } signalsTableBody.innerHTML = signals.map((s, i) => { const sector = (s.fundamental_data && s.fundamental_data.sector) || ''; const name = (s.fundamental_data && s.fundamental_data.name) || s.ticker; const narrative = s.narrative || s.reason || ''; const reasonSummary = s.reason_summary || s.reason || ''; const alphaColor = (s.alpha_score || 0) >= 70 ? 'gold' : ((s.alpha_score || 0) >= 50 ? 'green' : ''); return `
${s.ticker}${name !== s.ticker ? name : ''} ${sector ? '· ' + sector : ''}
${(s.signal_type || '').replace(/_/g, ' ').toUpperCase()} ${s.alpha_score || '-'} ${fmtPrice(s.entry_price)} ${fmtPrice(s.target_1)} ${fmtPrice(s.target_2)} ${fmtPrice(s.target_3)} ${fmtPrice(s.stop_loss)} ${s.risk_reward || '-'} ${s.quantity || '-'} ${reasonSummary}
${narrative}
`; }).join(''); }; window._toggleNarrative = (idx) => { const row = document.getElementById(`narrative-${idx}`); const signalRow = document.querySelector(`.signal-row[data-idx="${idx}"]`); if (!row) return; const isVisible = row.style.display !== 'none'; row.style.display = isVisible ? 'none' : 'table-row'; signalRow.classList.toggle('expanded', !isVisible); }; // --- Load Dashboard --- const loadDashboard = async () => { try { const res = await fetch(`${API_BASE}/dashboard?t=${Date.now()}`); const data = await res.json(); if (data.status === 'success') { updateStats(data.stats); updateTable(data.signals); if (data.pipeline_stages) { _pipelineStages = data.pipeline_stages; renderPipeline(data.pipeline_stages); } renderRegimes(data.regimes); if (data.last_scan_date) { lastUpdated.innerText = `Last Scan: ${new Date(data.last_scan_date).toLocaleString()}`; } } } catch (err) { console.error('Failed to load dashboard:', err); } }; // --- Scan Progress Simulation --- const pipelineStepNames = [ 'Determining universe...', 'Computing technical features...', 'Detecting signals...', 'Analyzing market regime...', 'Running fundamental checks...', 'Fetching news & sentiment...', 'Applying risk filters...', 'Sizing positions...', 'Scoring & ranking...', 'Building trade plans...' ]; let progressInterval = null; const showProgress = () => { scanProgress.classList.add('active'); let step = 0; progressStep.innerText = pipelineStepNames[0]; progressBar.style.width = '5%'; progressInterval = setInterval(() => { step++; if (step < pipelineStepNames.length) { progressStep.innerText = pipelineStepNames[step]; progressBar.style.width = `${(step / pipelineStepNames.length) * 90 + 5}%`; } }, 3000); }; const hideProgress = () => { progressBar.style.width = '100%'; setTimeout(() => { scanProgress.classList.remove('active'); if (progressInterval) clearInterval(progressInterval); progressBar.style.width = '0%'; }, 400); }; // --- Trigger Scans --- const triggerScan = async (market) => { const btn = market === 'us' ? btnScanUS : btnScanIN; const originalText = btn.innerHTML; btn.innerHTML = ` Scanning...`; btn.disabled = true; btnScanUS.disabled = true; btnScanIN.disabled = true; showProgress(); try { const res = await fetch(`${API_BASE}/scan`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ market, top_n: 10 }) }); const data = await res.json(); hideProgress(); if (data.status === 'success') { updateStats(data.stats); if (data.pipeline_stages) { _pipelineStages = data.pipeline_stages; renderPipeline(data.pipeline_stages); } renderRegimes(data.regimes); updateTable(data.signals); lastUpdated.innerText = `Last Scan: ${new Date().toLocaleString()}`; } else { alert('Scan failed: ' + (data.message || 'Unknown error')); } } catch (err) { hideProgress(); console.error('Scan error:', err); alert('Scan error. Check console.'); } finally { btn.innerHTML = originalText; btn.disabled = false; btnScanUS.disabled = false; btnScanIN.disabled = false; } }; btnScanUS.addEventListener('click', () => triggerScan('us')); btnScanIN.addEventListener('click', () => triggerScan('india')); // --- Chat Logic --- const toggleChat = () => chatPanel.classList.toggle('open'); chatFab.addEventListener('click', toggleChat); chatClose.addEventListener('click', toggleChat); const appendMessage = (text, isUser = false) => { const div = document.createElement('div'); div.className = `chat-msg ${isUser ? 'chat-msg-user' : 'chat-msg-bot'}`; const formatted = text.replace(/\*\*(.*?)\*\*/g, '$1').replace(/\n/g, '
'); div.innerHTML = `
${formatted}
`; chatMessages.appendChild(div); chatMessages.scrollTop = chatMessages.scrollHeight; }; const sendMessage = async () => { const text = chatInput.value.trim(); if (!text) return; appendMessage(text, true); chatInput.value = ''; chatInput.disabled = true; chatSend.innerHTML = ''; try { const res = await fetch(`${API_BASE}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: text }) }); const data = await res.json(); appendMessage(data.status === 'success' ? data.response : `❌ Error: ${data.message}`); } catch (err) { appendMessage(`❌ Connection error.`); } finally { chatInput.disabled = false; chatSend.innerHTML = ''; chatInput.focus(); } }; chatSend.addEventListener('click', sendMessage); chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); }); // Initial load loadDashboard(); });