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'; 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; } } class MarkersPaneView { constructor(source) { this._source = source; } renderer() { return new MarkersRenderer(this._source); } zOrder() { return 'top'; } } 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; // Adjust coordinates to bitmap space based on pixel ratio const ratio = scope.horizontalPixelRatio; ctx.save(); for (const marker of markers) { const timeCoordinate = chart.timeScale().timeToCoordinate(marker.time); if (timeCoordinate === null) continue; // Figure out price coordinate let price = marker.price || marker.value; // If price wasn't specified but we have the series data, grab the candle high/low 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(); }); } } 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.simulationMarkers = []; this.avgPriceSeries = null; this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price } this.currentMouseTime = null; this.init(); } async loadDailyMAData() { try { // Use 1d interval for this calculation const interval = '1d'; let candles = this.allData.get(interval); if (!candles || candles.length < 125) { const response = await fetch(`/api/v1/candles?symbol=BTC&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); } // Recreate series to apply custom colors per point via LineSeries data 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.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.formatDate(timestamp * 1000); }, }, 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.avgPriceSeries = this.chart.addSeries(LightweightCharts.LineSeries, { color: '#00bcd4', lineWidth: 1, lineStyle: LightweightCharts.LineStyle.Solid, lastValueVisible: true, priceLineVisible: false, crosshairMarkerVisible: false, title: '', }); 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)); // Subscribe to crosshair movement for Best Moving Averages updates this.chart.subscribeCrosshairMove(param => { if (param.time) { this.currentMouseTime = param.time; this.renderTA(); } else { this.currentMouseTime = null; this.renderTA(); } }); 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.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(`/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 && !Number.isNaN(candle.time) && !Number.isNaN(candle.open) && !Number.isNaN(candle.high) && !Number.isNaN(candle.low) && !Number.isNaN(candle.close)) { 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?.(); 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); } } // 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 = `
Loading technical analysis... ${time}
`; return; } try { const response = await fetch(`/api/v1/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); // Merge simulation markers if present if (this.simulationMarkers && this.simulationMarkers.length > 0) { markers = [...markers, ...this.simulationMarkers]; } // CRITICAL: Filter out any markers with invalid timestamps before passing to chart markers = markers.filter(m => m && m.time !== null && m.time !== undefined && !isNaN(m.time)); // Re-sort combined markers by time markers.sort((a, b) => a.time - b.time); // Use custom primitive for markers in v5 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) : '-'; // Format params as "MA(44)" style let paramsStr = ''; if (indSignal.params !== null && indSignal.params !== undefined) { paramsStr = `(${indSignal.params})`; } return `
${indSignal.name}${paramsStr} ${signalIcon} ${indSignal.signal.toUpperCase()} ${lastSignalDate}
`; }).join('') : ''; const summaryBadge = ''; // Best Moving Averages Logic (1D based) let displayMA = { ma44: null, ma125: null, price: null, time: null }; if (this.currentMouseTime && this.dailyMAData.size > 0) { // Find the 1D candle that includes this mouse time const dayTimestamp = Math.floor(this.currentMouseTime / 86400) * 86400; if (this.dailyMAData.has(dayTimestamp)) { displayMA = { ...this.dailyMAData.get(dayTimestamp), time: dayTimestamp }; } else { // Fallback to latest if specific day not found 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.resistance.toFixed(2)}
Support ${data.levels.support.toFixed(2)}
Price Position
${data.levels.position_in_range.toFixed(0)}% in range
`; } renderSignalsSection() { return ''; } async loadStats() { try { const response = await fetch('/api/v1/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', }); } document.getElementById('currentPrice').textContent = price.toFixed(2); 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(2); document.getElementById('dailyLow').textContent = this.statsData.low_24h.toFixed(2); } } switchTimeframe(interval) { if (!this.intervals.includes(interval) || interval === this.currentInterval) return; const oldInterval = this.currentInterval; this.currentInterval = interval; this.hasInitialLoad = false; document.querySelectorAll('.timeframe-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.interval === interval); }); // Clear indicator caches and signal state before switching timeframe this.clearIndicatorCaches(true); // Clear old interval data, not new interval this.allData.delete(oldInterval); this.lastCandleTimestamp = null; this.loadInitialData(); window.clearSimulationResults?.(); window.updateTimeframeDisplay?.(); // Notify indicators of timeframe change for recalculation window.onTimeframeChange?.(interval); } } 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'); }