import { INTERVALS, COLORS } from '../core/index.js'; 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.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, }, 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(); } }); } async loadInitialData() { await Promise.all([ this.loadData(1000, 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); } } 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) })); 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)); if (atEdge) { this.chart.timeScale().scrollToRealTime(); } const latest = chartData[chartData.length - 1]; this.updateStats(latest); } } 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(); 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); if (barsFromLeft < refillThreshold) { console.log(`Buffer low (${barsFromLeft} < ${refillThreshold.toFixed(0)}), silently prefetching ${bufferSize} candles...`); const oldestCandle = data[0]; if (oldestCandle) { this.loadHistoricalData(oldestCandle.time, bufferSize); } } } async loadHistoricalData(beforeTime, limit = 1000) { if (this.isLoading) { return; } this.isLoading = true; 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); this.candleSeries.setData(mergedData); // Recalculate indicators with the expanded dataset window.drawIndicatorsOnChart?.(); console.log(`Prefetched ${chartData.length} candles, total: ${mergedData.length}`); } else { console.log('No more historical data available'); } } catch (error) { console.error('Error loading historical data:', error); } finally { this.isLoading = false; } } async loadTA() { if (!this.hasInitialLoad) { const time = new Date().toLocaleTimeString(); document.getElementById('taContent').innerHTML = `