/**
* 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();
});