| document.addEventListener('DOMContentLoaded', () => { |
| |
| 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'); |
|
|
| |
| 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'); |
|
|
| |
| 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) : '-'; |
|
|
| |
| const renderPipeline = (stages) => { |
| if (!stages || stages.length === 0) { |
| pipelineFunnel.innerHTML = `<div style="text-align:center; padding:40px; color:var(--text-muted); width:100%;"> |
| <i class="fa-solid fa-play" style="margin-right:8px;"></i> Run a scan to see the filtering pipeline |
| </div>`; |
| 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 += ` |
| <div class="pipeline-stage ${stateClass}" data-stage-idx="${i}" onclick="window._showStageDetails(${i})"> |
| <div class="stage-icon"><i class="fa-solid ${stage.icon}"></i></div> |
| <div class="stage-label">${stage.label}</div> |
| <div class="stage-count" style="animation-delay:${i * 0.08}s">${stage.count_out}</div> |
| ${timeStr ? `<div class="stage-time">${timeStr}</div>` : ''} |
| </div>`; |
|
|
| if (i < stages.length - 1) { |
| const dropPct = stage.count_in > 0 ? Math.round((1 - stage.count_out / stage.count_in) * 100) : 0; |
| html += `<div class="stage-arrow"><i class="fa-solid fa-chevron-right"></i></div>`; |
| } |
| }); |
|
|
| pipelineFunnel.innerHTML = html; |
| }; |
|
|
| |
| let _pipelineStages = []; |
| window._showStageDetails = (idx) => { |
| const stage = _pipelineStages[idx]; |
| if (!stage) return; |
|
|
| |
| 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 = '<div class="details-tickers">' + |
| stage.tickers.slice(0, 30).map(t => `<span class="details-ticker-tag passed">${t}</span>`).join('') + |
| (stage.tickers.length > 30 ? `<span class="details-ticker-tag">+${stage.tickers.length - 30} more</span>` : '') + |
| '</div>'; |
| } |
|
|
| let rejectedHtml = ''; |
| if (stage.rejected && stage.rejected.length > 0) { |
| rejectedHtml = '<div class="rejected-list"><div style="font-size:0.75rem;color:var(--accent-rose);font-weight:600;margin-bottom:6px;"><i class="fa-solid fa-xmark"></i> Rejected:</div>' + |
| stage.rejected.slice(0, 10).map(r => |
| `<div class="rejected-item"><span class="ticker">${r.ticker}</span><span class="reason">${r.reason || r.signal_type || ''}</span></div>` |
| ).join('') + |
| (stage.rejected.length > 10 ? `<div style="font-size:0.7rem;color:var(--text-muted);padding:4px 0;">+${stage.rejected.length - 10} more</div>` : '') + |
| '</div>'; |
| } |
|
|
| pipelineDetails.innerHTML = ` |
| <div class="details-header"> |
| <div class="details-title"><i class="fa-solid ${stage.icon}"></i> ${stage.label}</div> |
| <div class="details-stats"> |
| <div class="stat"><i class="fa-solid fa-arrow-right-to-bracket"></i> In: ${stage.count_in}</div> |
| <div class="stat"><i class="fa-solid fa-arrow-right-from-bracket"></i> Out: ${stage.count_out}</div> |
| ${dropCount > 0 ? `<div class="stat" style="color:var(--accent-rose)"><i class="fa-solid fa-filter-circle-xmark"></i> Dropped: ${dropCount} (${dropPct}%)</div>` : ''} |
| ${stage.duration_ms > 0 ? `<div class="stat"><i class="fa-solid fa-clock"></i> ${(stage.duration_ms / 1000).toFixed(1)}s</div>` : ''} |
| </div> |
| </div> |
| <div class="details-body">${stage.details}</div> |
| ${tickerHtml} |
| ${rejectedHtml} |
| `; |
| pipelineDetails.classList.add('visible'); |
| }; |
|
|
| |
| 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 += ` |
| <div class="regime-card"> |
| <div class="regime-card-title"><i class="fa-solid fa-landmark"></i> ${mkt.toUpperCase()} Market Regime</div> |
| <span class="regime-badge ${trendClass}"><i class="fa-solid ${trendIcon}"></i> ${(r.regime || '').replace(/_/g, ' ').toUpperCase()}</span> |
| <div class="regime-info"> |
| Trend: <strong>${r.trend}</strong> | Volatility: ${r.volatility_state} | VIX: ${r.vix || 'N/A'} | Breadth: ${r.breadth || 'N/A'}<br> |
| ${r.description || ''} |
| </div> |
| </div>`; |
| } |
| regimeBanner.innerHTML = html; |
| }; |
|
|
| |
| 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` : '-'; |
| }; |
|
|
| |
| const updateTable = (signals) => { |
| if (!signals || signals.length === 0) { |
| signalsTableBody.innerHTML = `<tr><td colspan="12" style="text-align:center; padding:40px; color:var(--text-muted)">No opportunities found in the last scan.</td></tr>`; |
| 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 ` |
| <tr class="signal-row" data-idx="${i}" onclick="window._toggleNarrative(${i})"> |
| <td><span class="expand-indicator"><i class="fa-solid fa-chevron-right"></i></span></td> |
| <td><div class="ticker-cell"><span class="ticker-name">${s.ticker}</span><span class="ticker-sector">${name !== s.ticker ? name : ''} ${sector ? '· ' + sector : ''}</span></div></td> |
| <td><span class="badge">${(s.signal_type || '').replace(/_/g, ' ').toUpperCase()}</span></td> |
| <td><span class="badge ${alphaColor}">${s.alpha_score || '-'}</span></td> |
| <td class="mono">${fmtPrice(s.entry_price)}</td> |
| <td class="mono positive">${fmtPrice(s.target_1)}</td> |
| <td class="mono positive">${fmtPrice(s.target_2)}</td> |
| <td class="mono positive">${fmtPrice(s.target_3)}</td> |
| <td class="mono negative">${fmtPrice(s.stop_loss)}</td> |
| <td class="mono">${s.risk_reward || '-'}</td> |
| <td class="mono">${s.quantity || '-'}</td> |
| <td style="font-size:0.78rem; color:var(--text-muted); max-width:200px;">${reasonSummary}</td> |
| </tr> |
| <tr class="narrative-row" id="narrative-${i}" style="display:none;"> |
| <td colspan="12"><div class="narrative-container">${narrative}</div></td> |
| </tr>`; |
| }).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); |
| }; |
|
|
| |
| 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); } |
| }; |
|
|
| |
| 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); |
| }; |
|
|
| |
| const triggerScan = async (market) => { |
| const btn = market === 'us' ? btnScanUS : btnScanIN; |
| const originalText = btn.innerHTML; |
| btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin"></i> 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')); |
|
|
| |
| 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, '<strong>$1</strong>').replace(/\n/g, '<br>'); |
| div.innerHTML = `<div class="chat-bubble">${formatted}</div>`; |
| 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 = '<i class="fa-solid fa-ellipsis fa-bounce"></i>'; |
|
|
| 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 = '<i class="fa-solid fa-paper-plane"></i>'; |
| chatInput.focus(); |
| } |
| }; |
|
|
| chatSend.addEventListener('click', sendMessage); |
| chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); }); |
|
|
| |
| loadDashboard(); |
| }); |
|
|