Really-amin's picture
Upload 325 files
b66240d verified
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 = '<div class="combobox-option disabled">No results found</div>';
return;
}
this.symbolOptions.innerHTML = this.filteredSymbols.map(item => `
<div class="combobox-option" data-symbol="${item.symbol}">
<strong>${item.symbol}</strong>
<span>${item.name}</span>
</div>
`).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 = `
<div class="loading-spinner"></div>
<p>Loading ${symbol} chart data...</p>
`;
}
// 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 = `
<strong>Error loading chart:</strong>
<p>${result.error || 'Failed to load chart data'}</p>
<p><small>Symbol: ${symbol} | Timeframe: ${this.timeframe}</small></p>
`;
}
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 = '<strong>No data available</strong><p>No price data found for this symbol and timeframe.</p>';
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 = `
<div class="legend-item">
<span class="legend-label">Price</span>
<span class="legend-value">$${currentPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
</div>
<div class="legend-item">
<span class="legend-label">24h</span>
<span class="legend-value ${isUp ? 'positive' : 'negative'}">
<span class="legend-arrow">${isUp ? '↑' : '↓'}</span>
${isUp ? '+' : ''}${changePercent}%
</span>
</div>
<div class="legend-item">
<span class="legend-label">High</span>
<span class="legend-value">$${maxPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
</div>
<div class="legend-item">
<span class="legend-label">Low</span>
<span class="legend-value">$${minPrice.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span>
</div>
`;
}
} catch (error) {
console.error('Chart loading error:', error);
if (container) {
const errorNode = document.createElement('div');
errorNode.className = 'inline-message inline-error';
errorNode.innerHTML = `<strong>Error:</strong><p>${error.message || 'Failed to load chart'}</p>`;
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 = `
<div class="analysis-loading">
<div class="loading-spinner"></div>
<p>Running AI analysis with ${enabledIndicators.length > 0 ? enabledIndicators.join(', ') : 'default'} indicators...</p>
</div>
`;
try {
const result = await apiClient.analyzeChart(this.symbol, this.timeframe, enabledIndicators);
if (!result.ok) {
this.analysisOutput.innerHTML = `
<div class="inline-message inline-error">
<strong>Analysis Error:</strong>
<p>${result.error || 'Failed to run analysis'}</p>
</div>
`;
return;
}
const data = result.data || {};
const analysis = data.analysis || data;
if (!analysis) {
this.analysisOutput.innerHTML = '<div class="inline-message inline-warn">No AI insights returned.</div>';
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 `<li><strong>${key.toUpperCase()}:</strong> ${label} ${score !== '—' ? `(${score})` : ''}</li>`;
})
.join('');
this.analysisOutput.innerHTML = `
<div class="analysis-results">
<div class="analysis-header">
<h5>Analysis Results</h5>
<span class="analysis-badge ${direction.toLowerCase()}">${direction}</span>
</div>
<div class="analysis-metrics">
<div class="metric-item">
<span class="metric-label">Direction</span>
<span class="metric-value ${direction.toLowerCase()}">${direction}</span>
</div>
<div class="metric-item">
<span class="metric-label">Change</span>
<span class="metric-value ${changePercent >= 0 ? 'positive' : 'negative'}">
${changePercent >= 0 ? '+' : ''}${changePercent}%
</span>
</div>
<div class="metric-item">
<span class="metric-label">High</span>
<span class="metric-value">$${high}</span>
</div>
<div class="metric-item">
<span class="metric-label">Low</span>
<span class="metric-value">$${low}</span>
</div>
</div>
<div class="analysis-summary">
<h6>Summary</h6>
<p>${summary}</p>
</div>
${bullets ? `
<div class="analysis-signals">
<h6>Signals</h6>
<ul>${bullets}</ul>
</div>
` : ''}
</div>
`;
} catch (error) {
console.error('Analysis error:', error);
this.analysisOutput.innerHTML = `
<div class="inline-message inline-error">
<strong>Error:</strong>
<p>${error.message || 'Failed to run analysis'}</p>
</div>
`;
}
}
}
export default ChartLabView;