import { INTERVALS, COLORS } from '../core/index.js'; import { calculateAllIndicatorSignals, calculateSummarySignal } from './signals-calculator.js'; import { calculateSignalMarkers } from './signal-markers.js'; import { updateIndicatorCandles } from './indicators-panel-new.js'; import { TimezoneConfig } from '../config/timezone.js'; import { DrawingManager } from './drawing-tools.js'; class MarkersRenderer { constructor(source) { this._source = source; } draw(target) { if (!this._source._chart || !this._source._series) return; target.useBitmapCoordinateSpace((scope) => { const ctx = scope.context; const series = this._source._series; const chart = this._source._chart; const markers = this._source._markers; const ratio = scope.horizontalPixelRatio; ctx.save(); for (const marker of markers) { const timeCoordinate = chart.timeScale().timeToCoordinate(marker.time); if (timeCoordinate === null) continue; let price = marker.price || marker.value; if (!price && window.dashboard && window.dashboard.allData) { const data = window.dashboard.allData.get(window.dashboard.currentInterval); if (data) { const candle = data.find(d => d.time === marker.time); if (candle) { price = marker.position === 'aboveBar' ? candle.high : candle.low; } } } if (!price) continue; const priceCoordinate = series.priceToCoordinate(price); if (priceCoordinate === null) continue; const x = timeCoordinate * ratio; const size = 5 * ratio; const margin = 15 * ratio; const isAbove = marker.position === 'aboveBar'; const y = (isAbove ? priceCoordinate * ratio - margin : priceCoordinate * ratio + margin); ctx.fillStyle = marker.color || '#26a69a'; ctx.beginPath(); const shape = marker.shape || (isAbove ? 'arrowDown' : 'arrowUp'); if (shape === 'arrowUp' || shape === 'triangleUp') { ctx.moveTo(x, y - size); ctx.lineTo(x - size, y + size); ctx.lineTo(x + size, y + size); } else if (shape === 'arrowDown' || shape === 'triangleDown') { ctx.moveTo(x, y + size); ctx.lineTo(x - size, y - size); ctx.lineTo(x + size, y - size); } else if (shape === 'circle') { ctx.arc(x, y, size, 0, Math.PI * 2); } else if (shape === 'square') { ctx.rect(x - size, y - size, size * 2, size * 2); } else if (shape === 'custom' && marker.text) { ctx.font = `${Math.round(14 * ratio)}px Arial`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(marker.text, x, y); continue; } ctx.fill(); } ctx.restore(); }); } } class MarkersPaneView { constructor(source) { this._source = source; } renderer() { return new MarkersRenderer(this._source); } zOrder() { return 'top'; } } export class SeriesMarkersPrimitive { constructor(markers) { this._markers = markers || []; this._paneViews = [new MarkersPaneView(this)]; } setMarkers(markers) { this._markers = markers; if (this._requestUpdate) { this._requestUpdate(); } } attached(param) { this._chart = param.chart; this._series = param.series; this._requestUpdate = param.requestUpdate; this._requestUpdate(); } detached() { this._chart = undefined; this._series = undefined; this._requestUpdate = undefined; } updateAllViews() { this._requestUpdate?.(); } paneViews() { return this._paneViews; } } function formatDate(timestamp) { return TimezoneConfig.formatDate(timestamp); } function throttle(func, limit) { let inThrottle; return function() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } } } export class TradingDashboard { constructor() { this.chart = null; this.candleSeries = null; this.currentChartType = localStorage.getItem('winterfail_chart_type') || 'candlestick'; this.symbol = localStorage.getItem('winterfail_symbol') || 'BTC'; this.currentInterval = localStorage.getItem('winterfail_interval') || '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.simulationMarkers = []; this.avgPriceSeries = null; this.dailyMAData = new Map(); this.currentMouseTime = null; this.drawingManager = null; this.seriesMap = {}; this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150); this.init(); } async loadDailyMAData() { try { const interval = '1d'; let candles = this.allData.get(interval); if (!candles || candles.length < 125) { const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=${this.symbol}&interval=${interval}&limit=1000`); const data = await response.json(); if (data.candles && data.candles.length > 0) { candles = 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) })); this.allData.set(interval, candles); } } if (candles && candles.length >= 44) { const ma44 = this.calculateSimpleSMA(candles, 44); const ma125 = this.calculateSimpleSMA(candles, 125); this.dailyMAData.clear(); candles.forEach((c, i) => { this.dailyMAData.set(c.time, { price: c.close, ma44: ma44[i], ma125: ma125[i] }); }); } } catch (error) { console.error('[DailyMA] Error:', error); } } calculateSimpleSMA(candles, period) { const results = new Array(candles.length).fill(null); let sum = 0; for (let i = 0; i < candles.length; i++) { sum += candles[i].close; if (i >= period) sum -= candles[i - period].close; if (i >= period - 1) results[i] = sum / period; } return results; } setSimulationMarkers(markers) { this.simulationMarkers = markers || []; this.updateSignalMarkers(); } clearSimulationMarkers() { this.simulationMarkers = []; this.updateSignalMarkers(); } setAvgPriceData(data) { if (this.avgPriceSeries) { this.chart.removeSeries(this.avgPriceSeries); } this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, { lineWidth: 2, lineStyle: LightweightCharts.LineStyle.Solid, lastValueVisible: true, priceLineVisible: false, crosshairMarkerVisible: false, title: 'Avg Price', }); this.avgPriceSeries.setData(data || []); } clearAvgPriceData() { if (this.avgPriceSeries) { this.avgPriceSeries.setData([]); } } init() { this.createTimeframeButtons(); this.createChartTypeButtons(); 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); }); } createChartTypeButtons() { const container = document.querySelector('.flex.space-x-1:not(#timeframeContainer)'); if (!container) return; const chartTypes = [ { type: 'candlestick', icon: 'show_chart', name: 'Candlestick' }, { type: 'line', icon: 'insert_chart', name: 'Line' }, { type: 'bar', icon: 'scatter_plot', name: 'Bar' } ]; container.innerHTML = ''; chartTypes.forEach(chartType => { const btn = document.createElement('button'); btn.className = 'chart-type-btn w-10 h-10 flex items-center justify-center text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded transition-colors'; btn.dataset.chartType = chartType.type; btn.innerHTML = `${chartType.icon}`; btn.title = chartType.name; if (chartType.type === this.currentChartType) { btn.style.backgroundColor = '#2d3a4f'; btn.classList.add('text-blue-400'); } btn.addEventListener('click', () => this.switchChartType(chartType.type)); 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: COLORS.tvPanelBg }, separatorColor: COLORS.tvBorder, separatorHoverColor: COLORS.tvHover, enableResize: true } }, grid: { vertLines: { color: '#1e293b' }, horzLines: { color: '#1e293b' }, }, rightPriceScale: { borderColor: COLORS.tvBorder, autoScale: true, mode: 0, scaleMargins: { top: 0.1, bottom: 0.1, }, }, timeScale: { borderColor: COLORS.tvBorder, timeVisible: true, secondsVisible: false, rightOffset: 12, barSpacing: 10, tickMarkFormatter: (time, tickMarkType, locale) => { return TimezoneConfig.formatTickMark(time); }, }, localization: { timeFormatter: (timestamp) => { return TimezoneConfig.formatDate(timestamp * 1000); }, }, handleScroll: { mouseWheel: true, pressedMouseMove: true, horzTouchDrag: true, vertTouchDrag: true, }, handleScale: { axisPressedMouseMove: true, mouseWheel: true, pinch: true, }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal, }, }); const priceInput = document.getElementById("priceFormatInput"); let savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')); if (isNaN(savedPrecision)) savedPrecision = 2; if (priceInput) priceInput.value = savedPrecision; if (priceInput) { priceInput.addEventListener("input", (e) => { let precision = parseInt(e.target.value); if (isNaN(precision)) precision = 2; if (precision < 0) precision = 0; if (precision > 8) precision = 8; localStorage.setItem('winterfail_price_precision', precision); const minMove = precision === 0 ? 1 : Number((1 / Math.pow(10, precision)).toFixed(precision)); this.candleSeries.applyOptions({ priceFormat: { type: "price", precision: precision, minMove: minMove } }); }); } const savedUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800'; const savedDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800'; const candleUpInput = document.getElementById('candleUpColor'); const candleDownInput = document.getElementById('candleDownColor'); if (candleUpInput && this.currentChartType === 'candlestick') candleUpInput.value = savedUpColor; if (candleDownInput && this.currentChartType === 'candlestick') candleDownInput.value = savedDownColor; const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(savedPrecision)); this.candleSeries = this.addSeriesByType(this.currentChartType); if (this.currentChartType === 'line') { this.candleSeries.setData([]); } if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') { if (candleUpInput) { candleUpInput.addEventListener('input', (e) => { const color = e.target.value; localStorage.setItem('winterfail_candle_up', color); this.applyColorToChartType(color, 'up'); }); } if (candleDownInput) { candleDownInput.addEventListener('input', (e) => { const color = e.target.value; localStorage.setItem('winterfail_candle_down', color); this.applyColorToChartType(color, 'down'); }); } if (this.candleSeries) { this.currentPriceLine = this.candleSeries.createPriceLine({ price: 0, color: '#26a69a', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Dotted, axisLabelVisible: true, title: '', }); } } this.addAvgPriceSeries(); this.initPriceScaleControls(); this.initNavigationControls(); this.drawingManager = new DrawingManager(this, chartContainer); window.activateDrawingTool = (tool, event) => { const e = event || window.event; this.drawingManager.setTool(tool, e); }; document.addEventListener("DOMContentLoaded", () => { const priceSelect = document.getElementById("priceFormatSelect"); if (priceSelect) { priceSelect.addEventListener("change", (e) => { const precision = parseInt(e.target.value); this.chart.priceScale().applyOptions({ priceFormat: { type: "price", precision: precision, minMove: precision===0 ? 1 : 0.0001 } }); }); } }); this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.onVisibleRangeChange.bind(this)); this.chart.subscribeCrosshairMove(param => { if (param.time) { this.currentMouseTime = param.time; this.renderTA(); } else { this.currentMouseTime = null; this.renderTA(); } }); this.chart.subscribeClick(param => { window.hideAllPanels?.(); }); 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 btnSettings = document.getElementById('btnSettings'); const settingsPopup = document.getElementById('settingsPopup'); if (btnSettings && settingsPopup) { btnSettings.addEventListener('click', (e) => { e.stopPropagation(); settingsPopup.classList.toggle('hidden'); }); document.addEventListener('click', closeSettingsPopup); document.addEventListener('touchstart', closeSettingsPopup, { passive: true }); function closeSettingsPopup(e) { const isInside = settingsPopup.contains(e.target) || e.target === btnSettings; const isSettingsButton = e.target.closest('#btnSettings'); if (!isInside && !isSettingsButton) { settingsPopup.classList.add('hidden'); } } } this.scaleState = { autoScale: localStorage.getItem('winterfail_scale_auto') !== 'false', invertScale: localStorage.getItem('winterfail_scale_invert') === 'true', scaleMode: parseInt(localStorage.getItem('winterfail_scale_mode')) || 0 }; const updateCheckmark = (id, active) => { const el = document.getElementById(id); if (el) el.textContent = active ? '✓' : ''; }; const updateUI = () => { updateCheckmark('autoScaleCheck', this.scaleState.autoScale); updateCheckmark('invertScaleCheck', this.scaleState.invertScale); updateCheckmark('modeNormalCheck', this.scaleState.scaleMode === 0); updateCheckmark('modeLogCheck', this.scaleState.scaleMode === 1); updateCheckmark('modePercentCheck', this.scaleState.scaleMode === 2); updateCheckmark('modeIndexedCheck', this.scaleState.scaleMode === 3); this.candleSeries.priceScale().applyOptions({ autoScale: this.scaleState.autoScale, invertScale: this.scaleState.invertScale, mode: this.scaleState.scaleMode }); }; updateUI(); window.toggleScaleOption = (option) => { this.scaleState[option] = !this.scaleState[option]; localStorage.setItem(`winterfail_scale_${option}`, this.scaleState[option]); updateUI(); }; window.setScaleMode = (mode) => { this.scaleState.scaleMode = mode; localStorage.setItem('winterfail_scale_mode', mode); updateUI(); }; document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return; if (e.key.toLowerCase() === 'a') { window.toggleScaleOption('autoScale'); } else if (e.key.toLowerCase() === 'l') { const newMode = this.scaleState.scaleMode === 1 ? 0 : 1; window.setScaleMode(newMode); } }); } 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() { this.initChartTypeListeners(); 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(); } }); } initChartTypeListeners() { document.addEventListener('click', (e) => { const btn = e.target.closest('.chart-type-btn'); if (!btn) return; const chartType = btn.dataset.chartType; if (chartType) { this.switchChartType(chartType); } }); } clearIndicatorCaches(clearSignalState = false) { const activeIndicators = window.getActiveIndicators?.() || []; activeIndicators.forEach(indicator => { indicator.cachedResults = null; indicator.cachedMeta = null; 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.loadDailyMAData() ]); 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(`${window.APP_CONFIG.API_BASE_URL}/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); if (!this.candleSeries) { console.error('[Chart] Candle series not initialized'); return; } if (fitToContent) { this.chart.timeScale().scrollToRealTime(); } else if (visibleRange) { this.chart.timeScale().setVisibleLogicalRange(visibleRange); } if ((this.currentChartType === 'candlestick' || this.currentChartType === 'bar') && mergedData.length > 0 && mergedData[0].hasOwnProperty('open')) { this.candleSeries.setData(mergedData); } else if (this.currentChartType === 'line' && mergedData.length > 0 && mergedData[0].hasOwnProperty('close')) { const closePrices = mergedData.map(c => ({ time: c.time, value: c.close })); this.candleSeries.setData(closePrices); } else if (mergedData.length > 0 && mergedData[0].hasOwnProperty('value')) { this.candleSeries.setData(mergedData); } else if (mergedData.length > 0) { const closePrices = mergedData.map(c => ({ time: c.time, value: c.close || c.value })); this.candleSeries.setData(closePrices); } 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(`${window.APP_CONFIG.API_BASE_URL}/candles?symbol=BTC&interval=${this.currentInterval}&limit=50`); const data = await response.json(); if (!this.candleSeries) { console.error('[Chart] Candle series not initialized'); return; } 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]; const isNewCandle = this.lastCandleTimestamp !== null && latest.time > this.lastCandleTimestamp; if (isNewCandle) { console.log(`[NewData Load] New candle detected: ${this.lastCandleTimestamp} -> ${latest.time}`); this.clearIndicatorCaches(false); } this.lastCandleTimestamp = latest.time; if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') { chartData.forEach(candle => { if (candle.time >= lastTimestamp && !Number.isNaN(candle.time) && !Number.isNaN(candle.open) && !Number.isNaN(candle.high) && !Number.isNaN(candle.low) && !Number.isNaN(candle.close)) { this.candleSeries.update(candle); } }); } else if (this.currentChartType === 'line') { const closePrices = chartData.map(c => ({ time: c.time, value: c.close })); const existingData = this.candleSeries.data(); const existingTimeSet = new Set(existingData.map(d => d.time)); const newDataToAppend = closePrices.filter(c => !existingTimeSet.has(c.time)); if (newDataToAppend.length > 0) { if (existingData.length === 0) { this.candleSeries.setData(closePrices); } else { newDataToAppend.forEach(point => { this.candleSeries.update(point); }); } } } const existingData = this.allData.get(this.currentInterval) || []; this.allData.set(this.currentInterval, this.mergeData(existingData, chartData)); this.updateStats(latest); window.drawIndicatorsOnChart?.(); window.updateIndicatorCandles?.(); this.loadDailyMAData(); 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); } } if (data.length !== allData?.length) { console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`); } 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( `${window.APP_CONFIG.API_BASE_URL}/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()}`); if (this.currentChartType === 'candlestick' || this.currentChartType === 'bar') { if (mergedData.length > 0 && mergedData[0].hasOwnProperty('open')) { this.candleSeries.setData(mergedData); } else { const ohlcData = mergedData.map(c => ({ time: c.time, open: c.value, high: c.value, low: c.value, close: c.value })); this.candleSeries.setData(ohlcData); } } else { if (mergedData.length > 0 && mergedData[0].hasOwnProperty('close')) { const closePrices = mergedData.map(c => ({ time: c.time, value: c.close })); this.candleSeries.setData(closePrices); } else if (mergedData.length > 0) { this.candleSeries.setData(mergedData); } } 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 = `