import { INTERVALS, COLORS } from '../core/index.js'; import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals-calculator.js'; import { updateIndicatorCandles } from './indicators-panel-new.js'; import { TimezoneConfig } from '../config/timezone.js'; function formatDate(timestamp) { return TimezoneConfig.formatDate(timestamp); } export class TradingDashboard { constructor() { this.chart = null; this.candleSeries = null; this.currentInterval = '1d'; this.intervals = INTERVALS; this.allData = new Map(); this.isLoading = false; this.hasInitialLoad = false; this.taData = null; this.indicatorSignals = []; this.summarySignal = null; this.lastCandleTimestamp = null; this.init(); } init() { this.createTimeframeButtons(); this.initChart(); this.initEventListeners(); this.loadInitialData(); setInterval(() => { this.loadNewData(); this.loadStats(); if (new Date().getSeconds() < 15) this.loadTA(); }, 10000); } isAtRightEdge() { const timeScale = this.chart.timeScale(); const visibleRange = timeScale.getVisibleLogicalRange(); if (!visibleRange) return true; const data = this.candleSeries.data(); if (!data || data.length === 0) return true; return visibleRange.to >= data.length - 5; } createTimeframeButtons() { const container = document.getElementById('timeframeContainer'); container.innerHTML = ''; this.intervals.forEach(interval => { const btn = document.createElement('button'); btn.className = 'timeframe-btn'; btn.dataset.interval = interval; btn.textContent = interval; if (interval === this.currentInterval) { btn.classList.add('active'); } btn.addEventListener('click', () => this.switchTimeframe(interval)); container.appendChild(btn); }); } initChart() { const chartContainer = document.getElementById('chart'); this.chart = LightweightCharts.createChart(chartContainer, { layout: { background: { color: COLORS.tvBg }, textColor: COLORS.tvText, panes: { background: { color: '#1e222d' }, separatorColor: '#2a2e39', separatorHoverColor: '#363c4e', enableResize: true } }, grid: { vertLines: { color: '#363d4e' }, horzLines: { color: '#363d4e' }, }, rightPriceScale: { borderColor: '#363d4e', autoScale: true, }, timeScale: { borderColor: '#363d4e', timeVisible: true, secondsVisible: false, rightOffset: 12, barSpacing: 10, tickMarkFormatter: (time, tickMarkType, locale) => { return TimezoneConfig.formatTickMark(time); }, }, localization: { timeFormatter: (timestamp) => { return TimezoneConfig.formatTickMark(timestamp); }, }, handleScroll: { vertTouchDrag: false, }, }); this.candleSeries = this.chart.addSeries(LightweightCharts.CandlestickSeries, { upColor: '#ff9800', downColor: '#ff9800', borderUpColor: '#ff9800', borderDownColor: '#ff9800', wickUpColor: '#ff9800', wickDownColor: '#ff9800', lastValueVisible: false, priceLineVisible: false, }, 0); this.currentPriceLine = this.candleSeries.createPriceLine({ price: 0, color: '#26a69a', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Dotted, axisLabelVisible: true, title: '', }); this.initPriceScaleControls(); this.initNavigationControls(); this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this)); window.addEventListener('resize', () => { this.chart.applyOptions({ width: chartContainer.clientWidth, height: chartContainer.clientHeight, }); }); document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') { this.loadNewData(); this.loadTA(); } }); window.addEventListener('focus', () => { this.loadNewData(); this.loadTA(); }); } initPriceScaleControls() { const btnAutoScale = document.getElementById('btnAutoScale'); const btnLogScale = document.getElementById('btnLogScale'); if (!btnAutoScale || !btnLogScale) return; this.priceScaleState = { autoScale: true, logScale: false }; btnAutoScale.addEventListener('click', () => { this.priceScaleState.autoScale = !this.priceScaleState.autoScale; btnAutoScale.classList.toggle('active', this.priceScaleState.autoScale); this.candleSeries.priceScale().applyOptions({ autoScale: this.priceScaleState.autoScale }); console.log('Auto Scale:', this.priceScaleState.autoScale ? 'ON' : 'OFF'); }); btnLogScale.addEventListener('click', () => { this.priceScaleState.logScale = !this.priceScaleState.logScale; btnLogScale.classList.toggle('active', this.priceScaleState.logScale); let currentPriceRange = null; let currentTimeRange = null; if (!this.priceScaleState.autoScale) { try { currentPriceRange = this.candleSeries.priceScale().getVisiblePriceRange(); } catch (e) { console.log('Could not get price range'); } } try { currentTimeRange = this.chart.timeScale().getVisibleLogicalRange(); } catch (e) { console.log('Could not get time range'); } this.candleSeries.priceScale().applyOptions({ mode: this.priceScaleState.logScale ? LightweightCharts.PriceScaleMode.Logarithmic : LightweightCharts.PriceScaleMode.Normal }); this.chart.applyOptions({}); setTimeout(() => { if (currentTimeRange) { try { this.chart.timeScale().setVisibleLogicalRange(currentTimeRange); } catch (e) { console.log('Could not restore time range'); } } if (!this.priceScaleState.autoScale && currentPriceRange) { try { this.candleSeries.priceScale().setVisiblePriceRange(currentPriceRange); } catch (e) { console.log('Could not restore price range'); } } }, 100); console.log('Log Scale:', this.priceScaleState.logScale ? 'ON' : 'OFF'); }); document.addEventListener('keydown', (e) => { if (e.key === 'a' || e.key === 'A') { if (e.target.tagName !== 'INPUT') { btnAutoScale.click(); } } }); } initNavigationControls() { const chartWrapper = document.getElementById('chartWrapper'); const navLeft = document.getElementById('navLeft'); const navRight = document.getElementById('navRight'); const navRecent = document.getElementById('navRecent'); if (!chartWrapper || !navLeft || !navRight || !navRecent) return; chartWrapper.addEventListener('mousemove', (e) => { const rect = chartWrapper.getBoundingClientRect(); const distanceFromBottom = rect.bottom - e.clientY; chartWrapper.classList.toggle('show-nav', distanceFromBottom < 30); }); chartWrapper.addEventListener('mouseleave', () => { chartWrapper.classList.remove('show-nav'); }); navLeft.addEventListener('click', () => this.navigateLeft()); navRight.addEventListener('click', () => this.navigateRight()); navRecent.addEventListener('click', () => this.navigateToRecent()); } navigateLeft() { const visibleRange = this.chart.timeScale().getVisibleLogicalRange(); if (!visibleRange) return; const visibleBars = visibleRange.to - visibleRange.from; const shift = visibleBars * 0.8; const newFrom = visibleRange.from - shift; const newTo = visibleRange.to - shift; this.chart.timeScale().setVisibleLogicalRange({ from: newFrom, to: newTo }); } navigateRight() { const visibleRange = this.chart.timeScale().getVisibleLogicalRange(); if (!visibleRange) return; const visibleBars = visibleRange.to - visibleRange.from; const shift = visibleBars * 0.8; const newFrom = visibleRange.from + shift; const newTo = visibleRange.to + shift; this.chart.timeScale().setVisibleLogicalRange({ from: newFrom, to: newTo }); } navigateToRecent() { this.chart.timeScale().scrollToRealTime(); } initEventListeners() { document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return; const shortcuts = { '1': '1m', '2': '3m', '3': '5m', '4': '15m', '5': '30m', '7': '37m', '6': '1h', '8': '4h', '9': '8h', '0': '12h', 'd': '1d', 'D': '1d', 'w': '1w', 'W': '1w', 'm': '1M', 'M': '1M' }; if (shortcuts[e.key]) { this.switchTimeframe(shortcuts[e.key]); } if (e.key === 'ArrowLeft') { this.navigateLeft(); } else if (e.key === 'ArrowRight') { this.navigateRight(); } else if (e.key === 'ArrowUp') { this.navigateToRecent(); } }); } clearIndicatorCaches(clearSignalState = false) { const activeIndicators = window.getActiveIndicators?.() || []; activeIndicators.forEach(indicator => { // Always clear calculation caches indicator.cachedResults = null; indicator.cachedMeta = null; // Only clear signal state if explicitly requested (e.g., timeframe change) // Do not clear on new candle completion - preserve signal change tracking if (clearSignalState) { indicator.lastSignalTimestamp = null; indicator.lastSignalType = null; } }); console.log(`[Dashboard] Cleared caches for ${activeIndicators.length} indicators (signals: ${clearSignalState})`); } async loadInitialData() { await Promise.all([ this.loadData(2000, true), this.loadStats() ]); this.hasInitialLoad = true; this.loadTA(); } async loadData(limit = 1000, fitToContent = false) { if (this.isLoading) return; this.isLoading = true; try { const visibleRange = this.chart.timeScale().getVisibleLogicalRange(); const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=${limit}`); const data = await response.json(); if (data.candles && data.candles.length > 0) { const chartData = data.candles.reverse().map(c => ({ time: Math.floor(new Date(c.time).getTime() / 1000), open: parseFloat(c.open), high: parseFloat(c.high), low: parseFloat(c.low), close: parseFloat(c.close), volume: parseFloat(c.volume || 0) })); const existingData = this.allData.get(this.currentInterval) || []; const mergedData = this.mergeData(existingData, chartData); this.allData.set(this.currentInterval, mergedData); this.candleSeries.setData(mergedData); if (fitToContent) { this.chart.timeScale().scrollToRealTime(); } else if (visibleRange) { this.chart.timeScale().setVisibleLogicalRange(visibleRange); } const latest = mergedData[mergedData.length - 1]; this.updateStats(latest); } window.drawIndicatorsOnChart?.(); } catch (error) { console.error('Error loading data:', error); } finally { this.isLoading = false; } } async loadNewData() { if (!this.hasInitialLoad || this.isLoading) return; try { const response = await fetch(`/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`); const data = await response.json(); if (data.candles && data.candles.length > 0) { const atEdge = this.isAtRightEdge(); const currentSeriesData = this.candleSeries.data(); const lastTimestamp = currentSeriesData.length > 0 ? currentSeriesData[currentSeriesData.length - 1].time : 0; const chartData = data.candles.reverse().map(c => ({ time: Math.floor(new Date(c.time).getTime() / 1000), open: parseFloat(c.open), high: parseFloat(c.high), low: parseFloat(c.low), close: parseFloat(c.close), volume: parseFloat(c.volume || 0) })); const latest = chartData[chartData.length - 1]; // Check if new candle detected const isNewCandle = this.lastCandleTimestamp !== null && latest.time > this.lastCandleTimestamp; if (isNewCandle) { console.log(`[NewData Load] New candle detected: ${this.lastCandleTimestamp} -> ${latest.time}`); // Clear indicator caches but preserve signal state this.clearIndicatorCaches(false); } this.lastCandleTimestamp = latest.time; chartData.forEach(candle => { if (candle.time >= lastTimestamp) { this.candleSeries.update(candle); } }); const existingData = this.allData.get(this.currentInterval) || []; this.allData.set(this.currentInterval, this.mergeData(existingData, chartData)); //console.log(`[NewData Load] Added ${chartData.length} new candles, total in dataset: ${this.allData.get(this.currentInterval).length}`); if (atEdge) { this.chart.timeScale().scrollToRealTime(); } this.updateStats(latest); //console.log('[Chart] Calling drawIndicatorsOnChart after new data'); window.drawIndicatorsOnChart?.(); window.updateIndicatorCandles?.(); await this.loadSignals(); } } catch (error) { console.error('Error loading new data:', error); } } mergeData(existing, newData) { const dataMap = new Map(); existing.forEach(c => dataMap.set(c.time, c)); newData.forEach(c => dataMap.set(c.time, c)); return Array.from(dataMap.values()).sort((a, b) => a.time - b.time); } onVisibleRangeChange() { if (!this.hasInitialLoad || this.isLoading) { return; } const visibleRange = this.chart.timeScale().getVisibleLogicalRange(); if (!visibleRange) { return; } const data = this.candleSeries.data(); const allData = this.allData.get(this.currentInterval); if (!data || data.length === 0) { return; } const visibleBars = Math.ceil(visibleRange.to - visibleRange.from); const bufferSize = visibleBars * 2; const refillThreshold = bufferSize * 0.8; const barsFromLeft = Math.floor(visibleRange.from); const visibleOldestTime = data[Math.floor(visibleRange.from)]?.time; const visibleNewestTime = data[Math.ceil(visibleRange.to)]?.time; console.log(`[VisibleRange] Visible: ${visibleBars} bars (${data.length} in chart, ${allData?.length || 0} in dataset)`); console.log(`[VisibleRange] Time range: ${new Date((visibleOldestTime || 0) * 1000).toLocaleDateString()} to ${new Date((visibleNewestTime || 0) * 1000).toLocaleDateString()}`); if (barsFromLeft < refillThreshold) { console.log(`Buffer low (${barsFromLeft} < ${refillThreshold.toFixed(0)}), prefetching ${bufferSize} candles...`); const oldestCandle = data[0]; if (oldestCandle) { this.loadHistoricalData(oldestCandle.time, bufferSize); } } // Recalculate indicators when data changes if (data.length !== allData?.length) { console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`); } window.drawIndicatorsOnChart?.(); this.loadSignals().catch(e => console.error('Error loading signals:', e)); } async loadHistoricalData(beforeTime, limit = 1000) { if (this.isLoading) { return; } this.isLoading = true; console.log(`[Historical] Loading historical data before ${new Date(beforeTime * 1000).toLocaleDateString()}, limit=${limit}`); try { const endTime = new Date((beforeTime - 1) * 1000); const response = await fetch( `/api/v1/candles?symbol=BTC&interval=${this.currentInterval}&end=${endTime.toISOString()}&limit=${limit}` ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (data.candles && data.candles.length > 0) { const chartData = data.candles.reverse().map(c => ({ time: Math.floor(new Date(c.time).getTime() / 1000), open: parseFloat(c.open), high: parseFloat(c.high), low: parseFloat(c.low), close: parseFloat(c.close), volume: parseFloat(c.volume || 0) })); const existingData = this.allData.get(this.currentInterval) || []; const mergedData = this.mergeData(existingData, chartData); this.allData.set(this.currentInterval, mergedData); console.log(`[Historical] SUCCESS: Added ${chartData.length} candles`); console.log(`[Historical] Total candles in dataset: ${mergedData.length}`); console.log(`[Historical] Oldest: ${new Date(mergedData[0]?.time * 1000).toLocaleDateString()}`); console.log(`[Historical] Newest: ${new Date(mergedData[mergedData.length - 1]?.time * 1000).toLocaleDateString()}`); this.candleSeries.setData(mergedData); // Recalculate indicators and signals with the expanded dataset console.log(`[Historical] Recalculating indicators...`); window.drawIndicatorsOnChart?.(); await this.loadSignals(); console.log(`[Historical] Indicators recalculated for ${mergedData.length} candles`); } else { console.log('[Historical] No more historical data available from database'); } } catch (error) { console.error('[Historical] Error loading historical data:', error); } finally { this.isLoading = false; } } async loadTA() { if (!this.hasInitialLoad) { const time = new Date().toLocaleTimeString(); document.getElementById('taContent').innerHTML = `