/** * BIST Predictor — Ana Uygulama Mantığı * Dashboard state yönetimi, hisse seçimi, gerçek zamanlı güncelleme. */ const App = { // ─── State ────────────────────────────────────────────────────────────── state: { selectedSymbol: null, activeHorizon: 10, chartRange: 90, stocks: [], stockDetail: null, isConnected: false, sortOption: 'name', activeCategory: 'all', }, // ─── Başlatma ─────────────────────────────────────────────────────────── async init() { console.log('BIST Predictor başlatılıyor...'); // Chart.js başlat Charts.init(); // Tarih göster this.updateDate(); setInterval(() => this.updateDate(), 60000); // Event listener'ları bağla this.bindEvents(); // SSE bağlantısı this.connectSSE(); // İlk veri yükleme await this.loadDashboard(); // Sistem durumu kontrol this.checkSystemStatus(); }, // ─── Event Binding ────────────────────────────────────────────────────── bindEvents() { // Hisse arama const searchInput = document.getElementById('stock-search'); if (searchInput) { searchInput.addEventListener('input', (e) => this.filterStocks(e.target.value)); } // Hisse Sıralama const sortSelect = document.getElementById('stock-sort'); if (sortSelect) { sortSelect.addEventListener('change', (e) => { this.state.sortOption = e.target.value; this.renderStockList(this.state.stocks); }); } // Kategori Filter (Tümü/Hisse/Emtia) document.querySelectorAll('.cat-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.cat-btn').forEach(b => { b.classList.remove('active'); b.style.background = 'var(--bg-card)'; b.style.color = 'var(--text-muted)'; b.style.borderColor = 'rgba(255,255,255,0.06)'; }); const target = e.target; target.classList.add('active'); target.style.background = 'rgba(0, 230, 118, 0.2)'; target.style.color = '#00e676'; target.style.borderColor = 'rgba(0, 230, 118, 0.3)'; this.state.activeCategory = target.dataset.cat; this.filterStocks(document.getElementById('stock-search').value); }); }); // Horizon tab'ları document.querySelectorAll('.horizon-tab').forEach(tab => { tab.addEventListener('click', (e) => { const horizon = parseInt(e.target.dataset.horizon); this.switchHorizon(horizon); }); }); // Grafik range butonları document.querySelectorAll('[data-range]').forEach(btn => { btn.addEventListener('click', (e) => { const range = parseInt(e.target.dataset.range); this.switchChartRange(range); }); }); // Yenile butonu const refreshBtn = document.getElementById('btn-refresh'); if (refreshBtn) { refreshBtn.addEventListener('click', () => this.refreshData()); } // Pencere boyutu değişikliği window.addEventListener('resize', () => Charts.resize()); }, // ─── Dashboard Yükleme ────────────────────────────────────────────────── async loadDashboard() { try { const data = await API.getDashboard(this.state.activeHorizon); if (data.stocks && data.stocks.length > 0) { this.state.stocks = data.stocks; this.renderStockList(data.stocks); // İlk hisseyi seç if (!this.state.selectedSymbol && data.stocks.length > 0) { this.selectStock(data.stocks[0].symbol); } } else { // Henüz veri yok, hisse listesini config'den al await this.loadStockList(); } } catch (error) { console.warn('Dashboard yükleme hatası:', error); await this.loadStockList(); } }, async loadStockList() { try { const data = await API.getStocks(); this.state.stocks = data.stocks || []; this.renderStockList(this.state.stocks); if (this.state.stocks.length > 0 && !this.state.selectedSymbol) { this.selectStock(this.state.stocks[0].symbol); } } catch (error) { console.error('Hisse listesi yükleme hatası:', error); this.renderStockListEmpty(); } }, // ─── Hisse Listesi Render ──────────────────────────────────────────────── renderStockList(stocks) { const listEl = document.getElementById('stock-list'); if (!listEl) return; if (!stocks || stocks.length === 0) { this.renderStockListEmpty(); return; } // Sıralama let displayStocks = [...stocks]; const sortOpt = this.state.sortOption || 'name'; displayStocks.sort((a, b) => { if (sortOpt === 'name') { return a.symbol.localeCompare(b.symbol); } else if (sortOpt === 'score') { const aScore = a.confidence_score || a.total_score || 0; const bScore = b.confidence_score || b.total_score || 0; return bScore - aScore; } else if (sortOpt === 'increase') { const aChange = a.predicted_change || 0; const bChange = b.predicted_change || 0; return bChange - aChange; } else if (sortOpt === 'decrease') { const aChange = a.predicted_change || 0; const bChange = b.predicted_change || 0; return aChange - bChange; } return 0; }); listEl.innerHTML = displayStocks.map(stock => { const symbol = stock.symbol; const displaySymbol = stock.name || symbol.replace('.IS', ''); const score = stock.confidence_score || stock.total_score; const price = stock.latest_price; const change = stock.predicted_change; const isActive = symbol === this.state.selectedSymbol; const assetType = stock.asset_type || 'Hisse'; let scoreClass = 'score-none'; let scoreText = '—'; if (score !== null && score !== undefined) { scoreText = Math.round(score); if (score >= 80) scoreClass = 'score-high'; else if (score >= 60) scoreClass = 'score-mid'; else if (score >= 40) scoreClass = 'score-low-mid'; else scoreClass = 'score-low'; } let changeHtml = ''; if (change !== undefined && change !== null && sortOpt !== 'name' && sortOpt !== 'score') { const changeClass = change >= 0 ? 'color: #00e676' : 'color: #ff5252'; const changeSign = change >= 0 ? '+' : ''; changeHtml = `
${changeSign}${change.toFixed(2)}%
`; } return `
${displaySymbol}
${price ? '₺' + price.toFixed(2) : '—'} ${changeHtml}
${scoreText}
`; }).join(''); }, renderStockListEmpty() { const listEl = document.getElementById('stock-list'); if (listEl) { listEl.innerHTML = `
Henüz veri yok.
`; } }, // ─── Hisse Seçimi ─────────────────────────────────────────────────────── async selectStock(symbol) { this.state.selectedSymbol = symbol; // Aktif item'ı güncelle document.querySelectorAll('.stock-item').forEach(item => { item.classList.toggle('active', item.dataset.symbol === symbol); }); // Detay yükle await this.loadStockDetail(symbol); }, async loadStockDetail(symbol) { try { const data = await API.getStockDetail(symbol, this.state.activeHorizon); this.state.stockDetail = data; this.renderStockDetail(data); } catch (error) { console.error(`${symbol} detay yükleme hatası:`, error); // Boş state göster this.renderEmptyDetail(symbol); } }, // ─── Detay Render ─────────────────────────────────────────────────────── renderStockDetail(data) { const displaySymbol = data.name || data.symbol.replace('.IS', ''); // Chart başlığı const chartTitle = document.getElementById('chart-title'); if (chartTitle) chartTitle.textContent = `${displaySymbol} — Fiyat & Tahmin`; // Son fiyat const priceVal = document.getElementById('stat-price-value'); const priceChange = document.getElementById('stat-price-change'); if (data.latest_price && priceVal) { priceVal.textContent = `₺${parseFloat(data.latest_price.close).toFixed(2)}`; // Değişim hesapla if (data.prices && data.prices.length >= 2) { const prev = data.prices[1]?.close || 0; const curr = data.latest_price.close; if (prev > 0) { const changePct = ((curr - prev) / prev * 100).toFixed(2); const changeAbs = (curr - prev).toFixed(2); priceChange.textContent = `${changePct >= 0 ? '+' : ''}${changePct}% (${changePct >= 0 ? '+' : ''}₺${changeAbs})`; priceChange.className = `stat-change ${changePct >= 0 ? 'positive' : 'negative'}`; } } } else if (priceVal) { priceVal.textContent = '—'; } // Tahmin const predVal = document.getElementById('stat-prediction-value'); const predChange = document.getElementById('stat-prediction-change'); if (data.predictions && data.predictions.length > 0 && predVal) { const latestPred = data.predictions[0]; predVal.textContent = `₺${parseFloat(latestPred.predicted_close).toFixed(2)}`; if (data.latest_price) { const curr = data.latest_price.close; const pred = latestPred.predicted_close; const changePct = ((pred - curr) / curr * 100).toFixed(2); predChange.textContent = `${changePct >= 0 ? '↑' : '↓'} ${Math.abs(changePct)}%`; predChange.className = `stat-change ${changePct >= 0 ? 'positive' : 'negative'}`; } } else if (predVal) { predVal.textContent = '—'; if (predChange) predChange.textContent = ''; } // Güven puanı if (data.confidence) { Charts.updateGauge(data.confidence.total_score || 0); Charts.updateMetricBars(data.confidence); // Yön doğruluğu stat const accVal = document.getElementById('stat-accuracy-value'); const accSub = document.getElementById('stat-accuracy-sub'); if (accVal) { accVal.textContent = `%${(data.confidence.direction_accuracy || 0).toFixed(0)}`; } if (accSub && data.confidence.details?.direction) { const dir = data.confidence.details.direction; accSub.textContent = `${dir.correct || 0}/${dir.total || 0} doğru tahmin`; } } else { Charts.updateGauge(0); Charts.updateMetricBars({}); } // Grafik Charts.renderPriceChart(data, this.state.chartRange); // Karşılaştırma tablosu this.renderComparisonTable(data.comparisons || []); }, renderEmptyDetail(symbol) { const displaySymbol = symbol.replace('.IS', ''); const chartTitle = document.getElementById('chart-title'); if (chartTitle) chartTitle.textContent = `${displaySymbol} — Veri Bekleniyor`; Charts.updateGauge(0); Charts.updateMetricBars({}); document.getElementById('stat-price-value').textContent = '—'; document.getElementById('stat-prediction-value').textContent = '—'; document.getElementById('stat-accuracy-value').textContent = '—'; }, // ─── Karşılaştırma Tablosu ────────────────────────────────────────────── renderComparisonTable(comparisons) { const tbody = document.getElementById('comparison-tbody'); if (!tbody) return; if (!comparisons || comparisons.length === 0) { tbody.innerHTML = 'Henüz karşılaştırma verisi yok'; return; } tbody.innerHTML = comparisons.slice(0, 20).map(c => { const predicted = parseFloat(c.predicted_close); const actual = c.actual_close ? parseFloat(c.actual_close) : null; const errorPct = actual ? ((Math.abs(predicted - actual) / actual) * 100).toFixed(2) : '—'; const errorClass = actual ? (Math.abs(predicted - actual) / actual * 100 < 3 ? 'positive' : (Math.abs(predicted - actual) / actual * 100 < 7 ? '' : 'negative')) : ''; const inBand = actual && c.quantile_p10 && c.quantile_p90 ? (actual >= c.quantile_p10 && actual <= c.quantile_p90) : null; const bandHtml = inBand === null ? '—' : (inBand ? '' : ''); return ` ${c.target_date || '—'} ₺${predicted.toFixed(2)} ${actual ? '₺' + actual.toFixed(2) : 'Bekliyor'} ${errorPct}${errorPct !== '—' ? '%' : ''} ${bandHtml} `; }).join(''); }, // ─── Hisse Filtreleme ─────────────────────────────────────────────────── filterStocks(query) { const q = (query || '').toUpperCase().trim(); const items = document.querySelectorAll('.stock-item'); const activeCat = this.state.activeCategory || 'all'; items.forEach(item => { const symbol = item.dataset.symbol || ''; const displaySymbol = item.querySelector('.stock-symbol').textContent || ''; const cat = item.dataset.cat || 'Hisse'; const matchesSearch = symbol.toUpperCase().includes(q) || displaySymbol.toUpperCase().includes(q); const matchesCat = activeCat === 'all' || cat === activeCat; item.style.display = (matchesSearch && matchesCat) ? '' : 'none'; }); }, // ─── Horizon Değiştirme ───────────────────────────────────────────────── async switchHorizon(horizon) { this.state.activeHorizon = horizon; // Tab güncelle document.querySelectorAll('.horizon-tab').forEach(tab => { tab.classList.toggle('active', parseInt(tab.dataset.horizon) === horizon); }); // Verileri yeniden yükle await this.loadDashboard(); if (this.state.selectedSymbol) { await this.loadStockDetail(this.state.selectedSymbol); } }, // ─── Chart Range Değiştirme ───────────────────────────────────────────── switchChartRange(range) { this.state.chartRange = range; document.querySelectorAll('[data-range]').forEach(btn => { btn.classList.toggle('active', parseInt(btn.dataset.range) === range); }); if (this.state.stockDetail) { Charts.renderPriceChart(this.state.stockDetail, range); } }, // ─── Veri Yenileme ────────────────────────────────────────────────────── async refreshData() { this.showToast('Veriler yenileniyor...', 'info'); try { await this.loadDashboard(); if (this.state.selectedSymbol) { await this.loadStockDetail(this.state.selectedSymbol); } this.showToast('Veriler güncellendi!', 'success'); } catch (error) { this.showToast('Güncelleme hatası: ' + error.message, 'error'); } }, // ─── İlk Veri Yükleme ─────────────────────────────────────────────────── async loadInitialData() { this.showLoading('Geçmiş veriler yükleniyor...', 'Bu işlem birkaç dakika sürebilir.'); try { await API.loadInitialData(); this.showToast('Veri yükleme başlatıldı!', 'success'); } catch (error) { this.hideLoading(); this.showToast('Veri yükleme hatası: ' + error.message, 'error'); } }, // ─── SSE Bağlantısı ───────────────────────────────────────────────────── connectSSE() { API.onSSE((type, data, timestamp) => { this.handleSSEEvent(type, data); }); API.connectSSE(); }, handleSSEEvent(type, data) { switch (type) { case 'connected': this.setStatus('online', 'Bağlı'); break; case 'disconnected': this.setStatus('offline', 'Bağlantı kesildi'); break; case 'heartbeat': this.setStatus('online', 'Bağlı'); break; case 'prediction': this.showToast( `${data.symbol?.replace('.IS', '')} tahmini tamamlandı (${data.index}/${data.total})`, 'info' ); break; case 'prediction_complete': this.hideLoading(); this.showToast( `Tahminler tamamlandı: ${data.success}/${data.total} başarılı`, 'success' ); this.loadDashboard(); break; case 'comparison_complete': this.showToast( `Karşılaştırma tamamlandı: ${data.updated} tahmin güncellendi`, 'success' ); this.loadDashboard(); break; case 'data_load': const progress = (data.index / data.total * 100).toFixed(0); this.updateLoadingProgress(progress, `${data.symbol?.replace('.IS', '')} (${data.index}/${data.total})`); break; case 'prediction_manual': this.showToast(`${data.symbol?.replace('.IS', '')} manuel tahmin tamamlandı`, 'success'); if (data.symbol === this.state.selectedSymbol) { this.loadStockDetail(data.symbol); } break; default: console.log('SSE event:', type, data); } }, // ─── Sistem Durumu ────────────────────────────────────────────────────── async checkSystemStatus() { try { const status = await API.getSystemStatus(); if (status.model?.cuda_available) { this.setStatus('online', `GPU: ${status.model.gpu_name || 'CUDA'}`); } else { this.setStatus('online', 'CPU Modu'); } } catch (error) { this.setStatus('offline', 'Bağlantı hatası'); } }, // ─── UI Yardımcıları ──────────────────────────────────────────────────── setStatus(state, text) { const dot = document.getElementById('status-dot'); const textEl = document.getElementById('status-text'); if (dot) { dot.className = `status-dot ${state}`; } if (textEl) { textEl.textContent = text; } this.state.isConnected = state === 'online'; }, updateDate() { const dateEl = document.getElementById('header-date'); if (dateEl) { const now = new Date(); const options = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }; dateEl.textContent = now.toLocaleDateString('tr-TR', options); } }, showToast(message, type = 'info') { const container = document.getElementById('toast-container'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = ` ${message} `; container.appendChild(toast); setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease-in forwards'; setTimeout(() => toast.remove(), 300); }, 4000); }, showLoading(title, message) { const overlay = document.getElementById('loading-overlay'); const titleEl = document.getElementById('loading-title'); const msgEl = document.getElementById('loading-message'); const progressBar = document.getElementById('loading-progress-bar'); if (overlay) overlay.style.display = 'flex'; if (titleEl) titleEl.textContent = title || 'İşlem devam ediyor...'; if (msgEl) msgEl.textContent = message || ''; if (progressBar) progressBar.style.display = 'block'; }, hideLoading() { const overlay = document.getElementById('loading-overlay'); if (overlay) overlay.style.display = 'none'; }, updateLoadingProgress(percent, text) { const fill = document.getElementById('loading-progress-fill'); const msg = document.getElementById('loading-message'); if (fill) fill.style.width = `${percent}%`; if (msg) msg.textContent = text || ''; }, }; // ─── Uygulama Başlatma ────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { App.init(); });