Spaces:
Sleeping
Sleeping
| /** | |
| * 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 = '<div class="loading-state">Awaiting Market Data...</div>'; | |
| 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 += ` | |
| <div class="mover-row" style="cursor:pointer;" onclick="openCompanyModal('${item.ticker}')"> | |
| <div class="mover-info"> | |
| <span class="mover-sym">${sym}</span> | |
| <span class="mover-name">${name}</span> | |
| </div> | |
| <div class="mover-score ${colorClass}"> | |
| ${fScore(item.score)} | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| } | |
| // ---------------------------------------------------- | |
| // 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 += ` | |
| <div class="news-card ${borderClass}" style="cursor:pointer;" onclick="openCompanyModal('${news.ticker}')"> | |
| <div class="news-top"> | |
| <span class="news-sym">${displayTicker}</span> | |
| <span class="news-scr ${colorClass}">${fScore(news.score)}</span> | |
| </div> | |
| <div class="news-headline">${news.headline}</div> | |
| <div class="news-meta"> | |
| <span>${news.source.replace('📰', '').replace('💬', '')}</span> | |
| <span>${news.time_ago}</span> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| } | |
| // ---------------------------------------------------- | |
| // 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(`<div class="ticker__item">${bulls[i].ticker.replace('SECTOR_', '')} <span class="up">▲ ${fScore(bulls[i].score)}</span></div>`); | |
| if (bears[i]) tapes.push(`<div class="ticker__item">${bears[i].ticker.replace('SECTOR_', '')} <span class="down">▼ ${fScore(bears[i].score)}</span></div>`); | |
| } | |
| // 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 += ` | |
| <div class="search-result-item" onclick="openCompanyModal('${item.ticker}')"> | |
| <div class="mover-info"> | |
| <span class="mover-sym">${item.ticker.replace('SECTOR_', '')}</span> | |
| <span class="mover-name">${item.name}</span> | |
| </div> | |
| <div class="mover-score ${colorClass}">${fScore(item.score)}</div> | |
| </div> | |
| `; | |
| }); | |
| searchResults.style.display = 'block'; | |
| } else { | |
| searchResults.innerHTML = '<div class="search-result-item"><span class="text-muted">No data found</span></div>'; | |
| 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 += ` | |
| <div class="news-card ${borderClass}" style="margin-bottom:0.5rem; animation:none;"> | |
| <div class="news-top"> | |
| <span class="news-sym">${news.source.replace('📰', '')}</span> | |
| <span class="news-scr ${colorClass}">${fScore(news.score)}</span> | |
| </div> | |
| <div class="news-headline" style="font-size:0.8rem;">${news.headline}</div> | |
| <div class="news-meta"> | |
| <span></span> | |
| <span>${news.time_ago}</span> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| } else { | |
| newsList.innerHTML = '<div class="text-muted">No specific news found. Driven by general market sentiment.</div>'; | |
| } | |
| // 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 } } | |
| } | |
| } | |
| }); | |
| } | |