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: ``, name: 'Candlestick' }, { type: 'line', icon: ``, name: 'Line' }, { type: 'bar', icon: ``, 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.classList.add('active'); } 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 = `
Loading technical analysis... ${time}
`; return; } try { const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/ta?symbol=BTC&interval=${this.currentInterval}`); const data = await response.json(); if (data.error) { document.getElementById('taContent').innerHTML = `
${data.error}
`; return; } this.taData = data; await this.loadSignals(); this.renderTA(); } catch (error) { console.error('Error loading TA:', error); document.getElementById('taContent').innerHTML = '
Failed to load technical analysis. Please check if the database has candle data.
'; } } async loadSignals() { try { this.indicatorSignals = calculateAllIndicatorSignals(); this.summarySignal = calculateSummarySignal(this.indicatorSignals); this.updateSignalMarkers(); } catch (error) { console.error('Error loading signals:', error); this.indicatorSignals = []; this.summarySignal = null; } } updateSignalMarkers() { const candles = this.allData.get(this.currentInterval); if (!candles || candles.length === 0) return; let markers = calculateSignalMarkers(candles); if (this.simulationMarkers && this.simulationMarkers.length > 0) { markers = [...markers, ...this.simulationMarkers]; } markers = markers.filter(m => m && m.time !== null && m.time !== undefined && !isNaN(m.time)); markers.sort((a, b) => a.time - b.time); try { if (!this.markerPrimitive) { this.markerPrimitive = new SeriesMarkersPrimitive(); this.candleSeries.attachPrimitive(this.markerPrimitive); } this.markerPrimitive.setMarkers(markers); } catch (e) { console.warn('[SignalMarkers] setMarkers primitive error:', e.message); } } renderTA() { if (!this.taData || this.taData.error) { document.getElementById('taContent').innerHTML = `
${this.taData?.error || 'No data available'}
`; return; } const data = this.taData; const trendClass = data.trend.direction.toLowerCase(); const signalClass = data.trend.signal.toLowerCase(); document.getElementById('taInterval').textContent = this.currentInterval.toUpperCase(); document.getElementById('taLastUpdate').textContent = new Date().toLocaleTimeString(); const summary = this.summarySignal || {}; const summarySignalClass = summary.signal || 'hold'; const signalsHtml = this.indicatorSignals?.length > 0 ? this.indicatorSignals.map(indSignal => { const signalIcon = indSignal.signal === 'buy' ? '🟢' : indSignal.signal === 'sell' ? '🔴' : '⚪'; const signalColor = indSignal.signal === 'buy' ? '#26a69a' : indSignal.signal === 'sell' ? '#ef5350' : '#787b86'; const lastSignalDate = indSignal.lastSignalDate ? formatDate(indSignal.lastSignalDate * 1000) : '-'; let paramsStr = ''; if (indSignal.params !== null && indSignal.params !== undefined) { paramsStr = `(${indSignal.params})`; } return `
${indSignal.name}${paramsStr} ${signalIcon} ${indSignal.signal.toUpperCase()} ${lastSignalDate}
`; }).join('') : ''; const summaryBadge = ''; let displayMA = { ma44: null, ma125: null, price: null, time: null }; if (this.currentMouseTime && this.dailyMAData.size > 0) { const dayTimestamp = Math.floor(this.currentMouseTime / 86400) * 86400; if (this.dailyMAData.has(dayTimestamp)) { displayMA = { ...this.dailyMAData.get(dayTimestamp), time: dayTimestamp }; } else { const keys = Array.from(this.dailyMAData.keys()).sort((a, b) => b - a); const latestKey = keys[0]; displayMA = { ...this.dailyMAData.get(latestKey), time: latestKey }; } } else if (this.dailyMAData.size > 0) { const keys = Array.from(this.dailyMAData.keys()).sort((a, b) => b - a); const latestKey = keys[0]; displayMA = { ...this.dailyMAData.get(latestKey), time: latestKey }; } const ma44Value = displayMA.ma44; const ma125Value = displayMA.ma125; const currentPrice = displayMA.price; const ma44Change = (ma44Value && currentPrice) ? ((currentPrice - ma44Value) / ma44Value * 100) : null; const ma125Change = (ma125Value && currentPrice) ? ((currentPrice - ma125Value) / ma125Value * 100) : null; const maDateStr = displayMA.time ? TimezoneConfig.formatDate(displayMA.time * 1000).split(' ')[0] : 'Latest'; document.getElementById('taContent').innerHTML = `
Indicator Analysis ${summaryBadge}
${signalsHtml ? signalsHtml : `
No indicators selected. Add indicators from the sidebar panel to view signals.
`}
Best Moving Averages ${maDateStr} (1D)
MA 44 ${ma44Value ? ma44Value.toFixed(2) : 'N/A'} ${ma44Change !== null ? `${ma44Change >= 0 ? '+' : ''}${ma44Change.toFixed(1)}%` : ''}
MA 125 ${ma125Value ? ma125Value.toFixed(2) : 'N/A'} ${ma125Change !== null ? `${ma125Change >= 0 ? '+' : ''}${ma125Change.toFixed(1)}%` : ''}
Support / Resistance
Resistance ${data.levels ? data.levels.resistance.toFixed(2) : 'N/A'}
Support ${data.levels ? data.levels.support.toFixed(2) : 'N/A'}
Price Position
${data.levels ? data.levels.position_in_range.toFixed(0) : '--'}% in range
`; } renderSignalsSection() { return ''; } async loadStats() { try { const response = await fetch(`${window.APP_CONFIG.API_BASE_URL}/stats?symbol=BTC`); this.statsData = await response.json(); } catch (error) { console.error('Error loading stats:', error); } } updateStats(candle) { const price = candle.close; const isUp = candle.close >= candle.open; if (this.currentPriceLine) { this.currentPriceLine.applyOptions({ price: price, color: isUp ? '#26a69a' : '#ef5350', }); } const savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')) || 2; document.getElementById('currentPrice').textContent = price.toFixed(savedPrecision); if (this.statsData) { const change = this.statsData.change_24h; document.getElementById('currentPrice').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative'); document.getElementById('priceChange').textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%'; document.getElementById('priceChange').className = 'stat-value ' + (change >= 0 ? 'positive' : 'negative'); document.getElementById('dailyHigh').textContent = this.statsData.high_24h.toFixed(savedPrecision); document.getElementById('dailyLow').textContent = this.statsData.low_24h.toFixed(savedPrecision); } } switchTimeframe(interval) { if (!this.intervals.includes(interval) || interval === this.currentInterval) return; const oldInterval = this.currentInterval; this.currentInterval = interval; localStorage.setItem('winterfail_interval', interval); this.hasInitialLoad = false; document.querySelectorAll('.timeframe-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.interval === interval); }); this.clearIndicatorCaches(true); this.allData.delete(oldInterval); this.lastCandleTimestamp = null; this.loadInitialData(); window.clearSimulationResults?.(); window.updateTimeframeDisplay?.(); window.onTimeframeChange?.(interval); } switchChartType(chartType) { if (chartType === this.currentChartType) return; this.currentChartType = chartType; localStorage.setItem('winterfail_chart_type', chartType); const allData = this.allData.get(this.currentInterval) || []; const currentData = this.candleSeries ? this.candleSeries.data() : allData; this.chart.removeSeries(this.candleSeries); delete this.seriesMap.candlestick; if (this.avgPriceSeries) { this.chart.removeSeries(this.avgPriceSeries); this.avgPriceSeries = null; } if (this.currentPriceLine) { this.currentPriceLine.applyOptions({ visible: false }); } const newSeries = this.addSeriesByType(chartType); if (!newSeries) { console.error('[Chart] Failed to create series for type:', chartType); return; } this.candleSeries = newSeries; this.updateChartTypeButtons(); if (currentData && currentData.length > 0) { const chartData = this.allData.get(this.currentInterval) || currentData; const hasOHLC = chartData.length > 0 && chartData[0].hasOwnProperty('open'); if (chartType === 'candlestick' || chartType === 'bar') { if (hasOHLC) { this.candleSeries.setData(chartData); } else { const ohlcData = chartData.map(c => ({ time: c.time, open: c.value, high: c.value, low: c.value, close: c.value })); this.candleSeries.setData(ohlcData); } } else if (chartType === 'line') { const closePrices = chartData.length > 0 && chartData[0].hasOwnProperty('close') ? chartData.map(c => ({ time: c.time, value: c.close })) : chartData; this.candleSeries.setData(closePrices); } } window.drawIndicatorsOnChart?.(); } addSeriesByType(chartType) { let series; const savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')) || 2; const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(savedPrecision)); switch (chartType) { case 'candlestick': const candleUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800'; const candleDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800'; series = this.chart.addSeries(LightweightCharts.CandlestickSeries, { upColor: candleUpColor, downColor: candleDownColor, borderUpColor: candleUpColor, borderDownColor: candleDownColor, wickUpColor: candleUpColor, wickDownColor: candleDownColor, lastValueVisible: false, priceLineVisible: false, priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove } }); break; case 'line': series = this.chart.addSeries(LightweightCharts.LineSeries, { color: '#2196f3', lineWidth: 2, lineStyle: LightweightCharts.LineStyle.Solid, lastValueVisible: true, priceLineVisible: false, crosshairMarkerVisible: true, priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove } }); break; case 'bar': const barUpColor = localStorage.getItem('winterfail_candle_up') || '#ff9800'; const barDownColor = localStorage.getItem('winterfail_candle_down') || '#ff9800'; series = this.chart.addSeries(LightweightCharts.BarSeries, { upColor: barUpColor, downColor: barDownColor, barColors: { up: barUpColor, down: barDownColor }, lastValueVisible: false, priceLineVisible: false, priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove } }); break; } this.seriesMap[chartType] = series; return series; } addAvgPriceSeries() { const savedPrecision = parseInt(localStorage.getItem('winterfail_price_precision')) || 2; const initialMinMove = savedPrecision === 0 ? 1 : Number((1 / Math.pow(10, savedPrecision)).toFixed(savedPrecision)); this.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, { color: '#00bcd4', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Solid, lastValueVisible: true, priceLineVisible: false, crosshairMarkerVisible: false, title: '', priceFormat: { type: 'price', precision: savedPrecision, minMove: initialMinMove } }); } updateChartTypeButtons() { const buttons = document.querySelectorAll('.chart-type-btn'); buttons.forEach(btn => { btn.classList.remove('active'); if (btn.dataset.chartType === this.currentChartType) { btn.classList.add('active'); } }); } getChartTypeData() { if (!this.candleSeries) return []; return this.candleSeries.data(); } applyColorToChartType(color, direction) { if (!this.candleSeries) return; if (this.currentChartType === 'candlestick') { const options = {}; if (direction === 'up') { options.upColor = color; options.borderUpColor = color; options.wickUpColor = color; } else { options.downColor = color; options.borderDownColor = color; options.wickDownColor = color; } this.candleSeries.applyOptions(options); } else if (this.currentChartType === 'bar') { const options = { barColors: {} }; if (direction === 'up') { options.upColor = color; options.barColors.up = color; } else { options.downColor = color; options.barColors.down = color; } this.candleSeries.applyOptions(options); } } } export function refreshTA() { if (window.dashboard) { const time = new Date().toLocaleTimeString(); document.getElementById('taContent').innerHTML = `
Refreshing... ${time}
`; window.dashboard.loadTA(); } } export function openAIAnalysis() { const symbol = 'BTC'; const interval = window.dashboard?.currentInterval || '1d'; const prompt = `Analyze Bitcoin (${symbol}) ${interval} chart. Current trend, support/resistance levels, and trading recommendation. Technical indicators: MA44, MA125.`; const geminiUrl = `https://gemini.google.com/app?prompt=${encodeURIComponent(prompt)}`; window.open(geminiUrl, '_blank'); }