/** * Alpha Quantitative Engine — Dashboard Logic v2 */ // Global Chart Instances let sentimentBarChart = null; let moodDoughnutChart = null; const REFRESH_RATE_MS = 30000; // 30 seconds // Colors (matching CSS variables) const C_GREEN = '#00fa9a'; const C_RED = '#ff2a55'; const C_CYAN = '#00d2ff'; const C_MUTED = '#8b9bb4'; const BG_GREEN = 'rgba(0, 250, 154, 0.2)'; const BG_RED = 'rgba(255, 42, 85, 0.2)'; const BG_CYAN = 'rgba(0, 210, 255, 0.2)'; // Formatting const fScore = (num) => { const n = parseFloat(num); return (n > 0 ? '+' : '') + n.toFixed(4); }; // Set Global Chart Defaults Chart.defaults.color = C_MUTED; Chart.defaults.font.family = "'Inter', system-ui, sans-serif"; Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.05)'; document.addEventListener('DOMContentLoaded', () => { updateDashboard(); setInterval(updateDashboard, REFRESH_RATE_MS); document.getElementById('refresh-btn').addEventListener('click', (e) => { const icon = e.currentTarget.querySelector('i'); icon.classList.add('fa-spin'); updateDashboard().then(() => setTimeout(() => icon.classList.remove('fa-spin'), 600)); }); }); async function updateDashboard() { try { await Promise.all([ fetchStats(), fetchOverview(), fetchHeadlines() ]); console.log("Terminal Sync:", new Date().toLocaleTimeString()); } catch (err) { console.error("Sync Error:", err); } } // ---------------------------------------------------- // 1. Top Core Stats // ---------------------------------------------------- async function fetchStats() { const res = await fetch('/api/stats'); const data = await res.json(); document.getElementById('stat-stocks').textContent = data.stocks_scored.toLocaleString(); document.getElementById('stat-headlines').textContent = data.total_headlines.toLocaleString(); // Accuracy is static in HTML since it's from training. } // ---------------------------------------------------- // 2. Fetch Chart Data & Movers // ---------------------------------------------------- async function fetchOverview() { const res = await fetch('/api/overview'); const data = await res.json(); renderBarChart(data.bullish, data.bearish); renderMoodChart(data.bullish, data.bearish); renderMoversList('bullish-list', data.bullish, true); renderMoversList('bearish-list', data.bearish, false); renderTickerTape(data.bullish, data.bearish); } // ---------------------------------------------------- // 3. Main Bar Chart (Left Area) // ---------------------------------------------------- function renderBarChart(bullish, bearish) { const ctx = document.getElementById('sentimentBarChart').getContext('2d'); // Top 7 and Bottom 7 const chartData = [...bullish.slice(0, 7), ...bearish.slice(0, 7)]; chartData.sort((a, b) => b.score - a.score); // Highest to lowest const labels = chartData.map(d => d.ticker.replace('SECTOR_', '') + (d.ticker.includes('SECTOR') ? ' Sec' : '')); const scores = chartData.map(d => d.score); const bgColors = scores.map(s => s > 0.1 ? BG_GREEN : (s < -0.1 ? BG_RED : BG_CYAN)); const borderColors = scores.map(s => s > 0.1 ? C_GREEN : (s < -0.1 ? C_RED : C_CYAN)); if (sentimentBarChart) { sentimentBarChart.data.labels = labels; sentimentBarChart.data.datasets[0].data = scores; sentimentBarChart.data.datasets[0].backgroundColor = bgColors; sentimentBarChart.data.datasets[0].borderColor = borderColors; sentimentBarChart.update(); return; } sentimentBarChart = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'AI Score', data: scores, backgroundColor: bgColors, borderColor: borderColors, borderWidth: 1.5, borderRadius: 4, barPercentage: 0.6 }] }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(10, 14, 23, 0.95)', titleColor: '#fff', bodyColor: '#fff', borderColor: 'rgba(0, 210, 255, 0.3)', borderWidth: 1, padding: 12, displayColors: false, callbacks: { label: (ctx) => 'Score: ' + fScore(ctx.raw) } } }, scales: { x: { min: -1, max: 1 }, y: { grid: { display: false } } } } }); } // ---------------------------------------------------- // 4. Market Mood Doughnut Chart // ---------------------------------------------------- function renderMoodChart(bullish, bearish) { const ctx = document.getElementById('moodChart').getContext('2d'); // Simple math to derive overall market sentiment ratio const bullsCount = bullish.length; const bearsCount = bearish.length; // We assume the rest are neutral out of 50 total sample const neutralCount = Math.max(50 - (bullsCount + bearsCount), 5); const total = bullsCount + bearsCount + neutralCount; // Calculate an arbitrary overall index score (-1 to +1 based on ratios) const moodIndex = ((bullsCount - bearsCount) / total).toFixed(2); // Update center text const moodLabel = document.querySelector('#mood-label .mood-val'); moodLabel.textContent = (moodIndex > 0 ? '+' : '') + moodIndex; moodLabel.style.color = moodIndex > 0 ? C_GREEN : (moodIndex < 0 ? C_RED : C_CYAN); if (moodDoughnutChart) { moodDoughnutChart.data.datasets[0].data = [bullsCount, neutralCount, bearsCount]; moodDoughnutChart.update(); return; } moodDoughnutChart = new Chart(ctx, { type: 'doughnut', data: { labels: ['Bullish', 'Neutral', 'Bearish'], datasets: [{ data: [bullsCount, neutralCount, bearsCount], backgroundColor: [C_GREEN, C_CYAN, C_RED], borderWidth: 0, hoverOffset: 4 }] }, options: { responsive: true, maintainAspectRatio: false, cutout: '75%', plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(0,0,0,0.8)', padding: 10 } } } }); } // ---------------------------------------------------- // 5. Movers List (Bulls & Bears) // ---------------------------------------------------- function renderMoversList(elementId, items, isBullish) { const container = document.getElementById(elementId); container.innerHTML = ''; if (!items || items.length === 0) { container.innerHTML = '
Awaiting Market Data...
'; return; } const colorClass = isBullish ? 'text-green' : 'text-red'; items.slice(0, 5).forEach(item => { let sym = item.ticker; let name = item.name; if (sym.startsWith('SECTOR_')) { sym = sym.replace('SECTOR_', ''); name = sym + ' Sector Average'; } container.innerHTML += `
${sym} ${name}
${fScore(item.score)}
`; }); } // ---------------------------------------------------- // 6. Live News Feed Sidebar // ---------------------------------------------------- async function fetchHeadlines() { const res = await fetch('/api/headlines'); const headlines = await res.json(); const container = document.getElementById('news-feed'); container.innerHTML = ''; headlines.forEach(news => { const isBull = news.score > 0.2; const isBear = news.score < -0.2; const colorClass = isBull ? 'text-green' : (isBear ? 'text-red' : 'text-cyan'); const borderClass = isBull ? 'bullish' : (isBear ? 'bearish' : ''); let displayTicker = news.ticker.startsWith('SECTOR_') ? news.ticker.replace('SECTOR_', '') : news.ticker; container.innerHTML += `
${displayTicker} ${fScore(news.score)}
${news.headline}
${news.source.replace('📰', '').replace('💬', '')} ${news.time_ago}
`; }); } // ---------------------------------------------------- // 7. Ticker Tape (Top bar) // ---------------------------------------------------- function renderTickerTape(bulls, bears) { const container = document.getElementById('top-ticker'); // Interleave bulls and bears const tapes = []; const max = Math.max(bulls.length, bears.length); for (let i = 0; i < max; i++) { if (bulls[i]) tapes.push(`
${bulls[i].ticker.replace('SECTOR_', '')} ▲ ${fScore(bulls[i].score)}
`); if (bears[i]) tapes.push(`
${bears[i].ticker.replace('SECTOR_', '')} ▼ ${fScore(bears[i].score)}
`); } // Duplicate to ensure smooth infinite scroll container.innerHTML = tapes.join('') + tapes.join(''); } // ---------------------------------------------------- // 8. Live Search Bar // ---------------------------------------------------- const searchInput = document.getElementById('company-search'); const searchResults = document.getElementById('search-results'); let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); const query = e.target.value.trim(); if (query.length < 2) { searchResults.style.display = 'none'; return; } // Debounce API calls searchTimeout = setTimeout(async () => { try { const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`); const data = await res.json(); searchResults.innerHTML = ''; if (data.results && data.results.length > 0) { data.results.forEach(item => { const colorClass = item.score > 0.1 ? 'text-green' : (item.score < -0.1 ? 'text-red' : 'text-cyan'); searchResults.innerHTML += `
${item.ticker.replace('SECTOR_', '')} ${item.name}
${fScore(item.score)}
`; }); searchResults.style.display = 'block'; } else { searchResults.innerHTML = '
No data found
'; searchResults.style.display = 'block'; } } catch (err) { console.error("Search failed:", err); } }, 400); }); // Close search on click outside document.addEventListener('click', (e) => { if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) { searchResults.style.display = 'none'; } }); // ---------------------------------------------------- // 9. Company Details Modal // ---------------------------------------------------- let modalTrendChart = null; const modal = document.getElementById('company-modal'); const closeBtn = document.getElementById('modal-close'); closeBtn.addEventListener('click', () => modal.style.display = 'none'); modal.addEventListener('click', (e) => { if (e.target === modal) modal.style.display = 'none'; }); async function openCompanyModal(rawTicker) { // Hide search if open document.getElementById('search-results').style.display = 'none'; // Clean ticker const ticker = rawTicker.replace('SECTOR_', ''); try { const res = await fetch(`/api/company/${encodeURIComponent(ticker)}`); const data = await res.json(); // 1. Header Info document.getElementById('modal-ticker').textContent = data.ticker; document.getElementById('modal-name').textContent = data.name; document.getElementById('modal-sector').textContent = data.sector; const scoreEl = document.getElementById('modal-score'); scoreEl.textContent = fScore(data.current_score); scoreEl.className = 'stat-val ' + (data.current_score > 0.1 ? 'text-green' : (data.current_score < -0.1 ? 'text-red' : 'text-cyan')); // 2. Trend Chart renderModalChart(data.trend); // 3. News List const newsList = document.getElementById('modal-news-list'); newsList.innerHTML = ''; if (data.headlines && data.headlines.length > 0) { data.headlines.forEach(news => { const isBull = news.score > 0.2; const isBear = news.score < -0.2; const colorClass = isBull ? 'text-green' : (isBear ? 'text-red' : 'text-cyan'); const borderClass = isBull ? 'bullish' : (isBear ? 'bearish' : ''); newsList.innerHTML += `
${news.source.replace('📰', '')} ${fScore(news.score)}
${news.headline}
${news.time_ago}
`; }); } else { newsList.innerHTML = '
No specific news found. Driven by general market sentiment.
'; } // Show Modal modal.style.display = 'flex'; } catch (err) { console.error("Failed to load company details", err); } } function renderModalChart(trendData) { const ctx = document.getElementById('modalTrendChart').getContext('2d'); const labels = trendData.map(d => d.time_label); const scores = trendData.map(d => d.score); const isPositive = scores[scores.length - 1] >= 0; const lineColor = isPositive ? C_GREEN : C_RED; const bgColor = isPositive ? BG_GREEN : BG_RED; if (modalTrendChart) { modalTrendChart.data.labels = labels; modalTrendChart.data.datasets[0].data = scores; modalTrendChart.data.datasets[0].borderColor = lineColor; modalTrendChart.data.datasets[0].backgroundColor = bgColor; modalTrendChart.update(); return; } modalTrendChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [{ label: 'Sentiment Trend', data: scores, borderColor: lineColor, backgroundColor: bgColor, borderWidth: 2, fill: true, tension: 0.3, pointRadius: 3, pointBackgroundColor: '#fff' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(0,0,0,0.8)', padding: 10, callbacks: { label: (ctx) => 'Score: ' + fScore(ctx.raw) } } }, scales: { y: { min: -1, max: 1 }, x: { grid: { display: false } } } } }); }