import apiClient from './apiClient.js'; import errorHelper from './errorHelper.js'; import { createAdvancedLineChart, createCandlestickChart, createVolumeChart } from './tradingview-charts.js'; // Cryptocurrency symbols list const CRYPTO_SYMBOLS = [ { symbol: 'BTC', name: 'Bitcoin' }, { symbol: 'ETH', name: 'Ethereum' }, { symbol: 'BNB', name: 'Binance Coin' }, { symbol: 'SOL', name: 'Solana' }, { symbol: 'XRP', name: 'Ripple' }, { symbol: 'ADA', name: 'Cardano' }, { symbol: 'DOGE', name: 'Dogecoin' }, { symbol: 'DOT', name: 'Polkadot' }, { symbol: 'MATIC', name: 'Polygon' }, { symbol: 'AVAX', name: 'Avalanche' }, { symbol: 'LINK', name: 'Chainlink' }, { symbol: 'UNI', name: 'Uniswap' }, { symbol: 'LTC', name: 'Litecoin' }, { symbol: 'ATOM', name: 'Cosmos' }, { symbol: 'ALGO', name: 'Algorand' }, { symbol: 'TRX', name: 'Tron' }, { symbol: 'XLM', name: 'Stellar' }, { symbol: 'VET', name: 'VeChain' }, { symbol: 'FIL', name: 'Filecoin' }, { symbol: 'ETC', name: 'Ethereum Classic' }, { symbol: 'AAVE', name: 'Aave' }, { symbol: 'MKR', name: 'Maker' }, { symbol: 'COMP', name: 'Compound' }, { symbol: 'SUSHI', name: 'SushiSwap' }, { symbol: 'YFI', name: 'Yearn Finance' }, ]; class ChartLabView { constructor(section) { this.section = section; this.symbolInput = section.querySelector('[data-chart-symbol-input]'); this.symbolDropdown = section.querySelector('[data-chart-symbol-dropdown]'); this.symbolOptions = section.querySelector('[data-chart-symbol-options]'); this.timeframeButtons = section.querySelectorAll('[data-timeframe]'); this.indicatorButtons = section.querySelectorAll('[data-indicator]'); this.loadButton = section.querySelector('[data-load-chart]'); this.runAnalysisButton = section.querySelector('[data-run-analysis]'); this.canvas = section.querySelector('#price-chart'); this.analysisOutput = section.querySelector('[data-analysis-output]'); this.chartTitle = section.querySelector('[data-chart-title]'); this.chartLegend = section.querySelector('[data-chart-legend]'); this.chart = null; this.symbol = 'BTC'; this.timeframe = '7d'; this.filteredSymbols = [...CRYPTO_SYMBOLS]; } async init() { this.setupCombobox(); this.bindEvents(); await this.loadChart(); } setupCombobox() { if (!this.symbolInput || !this.symbolOptions) return; // Populate options this.renderOptions(); // Set initial value this.symbolInput.value = 'BTC - Bitcoin'; // Input event for filtering this.symbolInput.addEventListener('input', (e) => { const query = e.target.value.trim().toUpperCase(); this.filterSymbols(query); }); // Focus event to show dropdown this.symbolInput.addEventListener('focus', () => { this.symbolDropdown.style.display = 'block'; this.filterSymbols(this.symbolInput.value.trim().toUpperCase()); }); // Click outside to close document.addEventListener('click', (e) => { if (!this.symbolInput.contains(e.target) && !this.symbolDropdown.contains(e.target)) { this.symbolDropdown.style.display = 'none'; } }); } filterSymbols(query) { if (!query) { this.filteredSymbols = [...CRYPTO_SYMBOLS]; } else { this.filteredSymbols = CRYPTO_SYMBOLS.filter(item => item.symbol.includes(query) || item.name.toUpperCase().includes(query) ); } this.renderOptions(); } renderOptions() { if (!this.symbolOptions) return; if (this.filteredSymbols.length === 0) { this.symbolOptions.innerHTML = '
No results found
'; return; } this.symbolOptions.innerHTML = this.filteredSymbols.map(item => `
${item.symbol} ${item.name}
`).join(''); // Add click handlers this.symbolOptions.querySelectorAll('.combobox-option').forEach(option => { if (!option.classList.contains('disabled')) { option.addEventListener('click', () => { const symbol = option.dataset.symbol; const item = CRYPTO_SYMBOLS.find(i => i.symbol === symbol); if (item) { this.symbol = symbol; this.symbolInput.value = `${item.symbol} - ${item.name}`; this.symbolDropdown.style.display = 'none'; this.loadChart(); } }); } }); } bindEvents() { // Timeframe buttons this.timeframeButtons.forEach((btn) => { btn.addEventListener('click', async () => { this.timeframeButtons.forEach((b) => b.classList.remove('active')); btn.classList.add('active'); this.timeframe = btn.dataset.timeframe; await this.loadChart(); }); }); // Load chart button if (this.loadButton) { this.loadButton.addEventListener('click', async (e) => { e.preventDefault(); // Extract symbol from input const inputValue = this.symbolInput.value.trim(); if (inputValue) { const match = inputValue.match(/^([A-Z0-9]+)/); if (match) { this.symbol = match[1].toUpperCase(); } else { this.symbol = inputValue.toUpperCase(); } } await this.loadChart(); }); } // Indicator buttons if (this.indicatorButtons.length > 0) { this.indicatorButtons.forEach((btn) => { btn.addEventListener('click', () => { btn.classList.toggle('active'); // Don't auto-run, wait for Run Analysis button }); }); } // Run analysis button if (this.runAnalysisButton) { this.runAnalysisButton.addEventListener('click', async (e) => { e.preventDefault(); await this.runAnalysis(); }); } } async loadChart() { if (!this.canvas) return; const symbol = this.symbol.trim().toUpperCase() || 'BTC'; if (!symbol) { this.symbol = 'BTC'; if (this.symbolInput) this.symbolInput.value = 'BTC - Bitcoin'; } const container = this.canvas.closest('.chart-wrapper') || this.canvas.parentElement; // Show loading state if (container) { let loadingNode = container.querySelector('.chart-loading'); if (!loadingNode) { loadingNode = document.createElement('div'); loadingNode.className = 'chart-loading'; container.insertBefore(loadingNode, this.canvas); } loadingNode.innerHTML = `

Loading ${symbol} chart data...

`; } // Update title if (this.chartTitle) { this.chartTitle.textContent = `${symbol} Price Chart (${this.timeframe})`; } try { const result = await apiClient.getPriceChart(symbol, this.timeframe); // Remove loading if (container) { const loadingNode = container.querySelector('.chart-loading'); if (loadingNode) loadingNode.remove(); } if (!result.ok) { const errorAnalysis = errorHelper.analyzeError(new Error(result.error), { symbol, timeframe: this.timeframe }); if (container) { let errorNode = container.querySelector('.chart-error'); if (!errorNode) { errorNode = document.createElement('div'); errorNode.className = 'inline-message inline-error chart-error'; container.appendChild(errorNode); } errorNode.innerHTML = ` Error loading chart:

${result.error || 'Failed to load chart data'}

Symbol: ${symbol} | Timeframe: ${this.timeframe}

`; } return; } if (container) { const errorNode = container.querySelector('.chart-error'); if (errorNode) errorNode.remove(); } // Parse chart data const chartData = result.data || {}; const points = chartData.data || chartData || []; if (!points || points.length === 0) { if (container) { const errorNode = document.createElement('div'); errorNode.className = 'inline-message inline-warn'; errorNode.innerHTML = 'No data available

No price data found for this symbol and timeframe.

'; container.appendChild(errorNode); } return; } // Format labels and data const labels = points.map((point) => { const ts = point.time || point.timestamp || point.date; if (!ts) return ''; const date = new Date(ts); if (this.timeframe === '1d') { return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); } return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); }); const prices = points.map((point) => { const price = point.price || point.close || point.value || 0; return parseFloat(price) || 0; }); // Destroy existing chart if (this.chart) { this.chart.destroy(); } // Calculate min/max for better scaling const minPrice = Math.min(...prices); const maxPrice = Math.max(...prices); const priceRange = maxPrice - minPrice; const firstPrice = prices[0]; const lastPrice = prices[prices.length - 1]; const priceChange = lastPrice - firstPrice; const priceChangePercent = ((priceChange / firstPrice) * 100).toFixed(2); const isPriceUp = priceChange >= 0; // Get indicator states const showMA20 = this.section.querySelector('[data-indicator="MA20"]')?.checked || false; const showMA50 = this.section.querySelector('[data-indicator="MA50"]')?.checked || false; const showRSI = this.section.querySelector('[data-indicator="RSI"]')?.checked || false; const showVolume = this.section.querySelector('[data-indicator="Volume"]')?.checked || false; // Prepare price data for TradingView chart const priceData = points.map((point, index) => ({ time: point.time || point.timestamp || point.date || new Date().getTime() + (index * 60000), price: parseFloat(point.price || point.close || point.value || 0), volume: parseFloat(point.volume || 0) })); // Create TradingView-style chart with indicators this.chart = createAdvancedLineChart('chart-lab-canvas', priceData, { showMA20, showMA50, showRSI, showVolume }); // If volume is enabled, create separate volume chart if (showVolume && priceData.some(p => p.volume > 0)) { const volumeContainer = this.section.querySelector('[data-volume-chart]'); if (volumeContainer) { createVolumeChart('volume-chart-canvas', priceData); } } // Update legend with TradingView-style info if (this.chartLegend && prices.length > 0) { const currentPrice = prices[prices.length - 1]; const firstPrice = prices[0]; const change = currentPrice - firstPrice; const changePercent = ((change / firstPrice) * 100).toFixed(2); const isUp = change >= 0; this.chartLegend.innerHTML = `
Price $${currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
24h ${isUp ? '↑' : '↓'} ${isUp ? '+' : ''}${changePercent}%
High $${maxPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
Low $${minPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
`; } } catch (error) { console.error('Chart loading error:', error); if (container) { const errorNode = document.createElement('div'); errorNode.className = 'inline-message inline-error'; errorNode.innerHTML = `Error:

${error.message || 'Failed to load chart'}

`; container.appendChild(errorNode); } } } async runAnalysis() { if (!this.analysisOutput) return; const enabledIndicators = Array.from(this.indicatorButtons) .filter((btn) => btn.classList.contains('active')) .map((btn) => btn.dataset.indicator); this.analysisOutput.innerHTML = `

Running AI analysis with ${enabledIndicators.length > 0 ? enabledIndicators.join(', ') : 'default'} indicators...

`; try { const result = await apiClient.analyzeChart(this.symbol, this.timeframe, enabledIndicators); if (!result.ok) { this.analysisOutput.innerHTML = `
Analysis Error:

${result.error || 'Failed to run analysis'}

`; return; } const data = result.data || {}; const analysis = data.analysis || data; if (!analysis) { this.analysisOutput.innerHTML = '
No AI insights returned.
'; return; } const summary = analysis.summary || analysis.narrative?.summary || 'No summary available.'; const signals = analysis.signals || {}; const direction = analysis.change_direction || 'N/A'; const changePercent = analysis.change_percent ?? '—'; const high = analysis.high ?? '—'; const low = analysis.low ?? '—'; const bullets = Object.entries(signals) .map(([key, value]) => { const label = value?.label || value || 'n/a'; const score = value?.score ?? value?.value ?? '—'; return `
  • ${key.toUpperCase()}: ${label} ${score !== '—' ? `(${score})` : ''}
  • `; }) .join(''); this.analysisOutput.innerHTML = `
    Analysis Results
    ${direction}
    Direction ${direction}
    Change ${changePercent >= 0 ? '+' : ''}${changePercent}%
    High $${high}
    Low $${low}
    Summary

    ${summary}

    ${bullets ? `
    Signals
    ` : ''}
    `; } catch (error) { console.error('Analysis error:', error); this.analysisOutput.innerHTML = `
    Error:

    ${error.message || 'Failed to run analysis'}

    `; } } } export default ChartLabView;