/** * Services Page - Technical Indicator Services */ class ServicesPage { constructor() { this.services = []; this.currentCategory = 'all'; this.currentSymbol = 'BTC'; this.currentTimeframe = '1h'; } async init() { console.log('[Services] Initializing...'); this.bindEvents(); await this.loadServices(); this.checkUrlParams(); console.log('[Services] Ready'); } bindEvents() { // Refresh button document.getElementById('refresh-btn')?.addEventListener('click', () => { this.loadServices(); }); // Symbol input document.getElementById('symbol-input')?.addEventListener('change', (e) => { this.currentSymbol = e.target.value.toUpperCase() || 'BTC'; }); // Timeframe select document.getElementById('timeframe-select')?.addEventListener('change', (e) => { this.currentTimeframe = e.target.value || '1h'; }); // Analyze all button document.getElementById('analyze-all-btn')?.addEventListener('click', () => { this.analyzeAll(); }); // Category buttons document.querySelectorAll('.category-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); this.currentCategory = e.target.dataset.category; this.filterServices(); }); }); } checkUrlParams() { const params = new URLSearchParams(window.location.search); const service = params.get('service'); if (service) { // Auto-analyze the specific service setTimeout(() => { this.analyzeService(service); }, 500); } } async loadServices() { const grid = document.getElementById('services-grid'); if (!grid) return; grid.innerHTML = `

Loading indicator services...

`; try { const response = await fetch('/api/indicators/services'); if (response.ok) { const data = await response.json(); this.services = data.services || []; console.log('[Services] Loaded', this.services.length, 'services'); } else { // Use fallback data this.services = this.getFallbackServices(); } } catch (error) { console.error('[Services] Load error:', error); this.services = this.getFallbackServices(); } this.renderServices(); this.updateTimestamp(); } getFallbackServices() { return [ { id: 'bollinger_bands', name: 'Bollinger Bands', description: 'Volatility bands placed above and below a moving average. Identifies overbought/oversold conditions and potential breakouts.', endpoint: '/api/indicators/bollinger-bands', parameters: ['symbol', 'timeframe', 'period', 'std_dev'], icon: '📊', category: 'volatility' }, { id: 'stoch_rsi', name: 'Stochastic RSI', description: 'Combines Stochastic oscillator with RSI for enhanced momentum detection. Great for identifying extreme conditions.', endpoint: '/api/indicators/stoch-rsi', parameters: ['symbol', 'timeframe', 'rsi_period', 'stoch_period'], icon: '📈', category: 'momentum' }, { id: 'atr', name: 'Average True Range (ATR)', description: 'Measures market volatility by analyzing the range of price movements. Useful for setting stop losses.', endpoint: '/api/indicators/atr', parameters: ['symbol', 'timeframe', 'period'], icon: '📉', category: 'volatility' }, { id: 'sma', name: 'Simple Moving Average (SMA)', description: 'Average price over specified periods (20, 50, 200). Identifies trend direction and support/resistance levels.', endpoint: '/api/indicators/sma', parameters: ['symbol', 'timeframe'], icon: '〰️', category: 'trend' }, { id: 'ema', name: 'Exponential Moving Average (EMA)', description: 'Weighted moving average giving more weight to recent prices. More responsive to current price action.', endpoint: '/api/indicators/ema', parameters: ['symbol', 'timeframe'], icon: '📐', category: 'trend' }, { id: 'macd', name: 'MACD', description: 'Moving Average Convergence Divergence. Trend-following momentum indicator showing relationship between EMAs.', endpoint: '/api/indicators/macd', parameters: ['symbol', 'timeframe', 'fast', 'slow', 'signal'], icon: '🔀', category: 'momentum' }, { id: 'rsi', name: 'RSI', description: 'Relative Strength Index. Momentum oscillator measuring speed and magnitude of price movements (0-100).', endpoint: '/api/indicators/rsi', parameters: ['symbol', 'timeframe', 'period'], icon: '💪', category: 'momentum' }, { id: 'comprehensive', name: 'Comprehensive Analysis', description: 'All indicators combined with trading signals. Get a complete market overview with actionable recommendations.', endpoint: '/api/indicators/comprehensive', parameters: ['symbol', 'timeframe'], icon: '🎯', category: 'analysis' } ]; } filterServices() { this.renderServices(); } renderServices() { const grid = document.getElementById('services-grid'); if (!grid) return; const filteredServices = this.currentCategory === 'all' ? this.services : this.services.filter(s => s.category === this.currentCategory); if (filteredServices.length === 0) { grid.innerHTML = `

No services found

No indicator services match the selected category.

`; return; } grid.innerHTML = filteredServices.map(service => `
${service.icon}

${service.name}

${service.category}

${service.description}

${service.parameters.map(p => `${p}`).join('')}
`).join(''); } async analyzeService(serviceId) { const resultsSection = document.getElementById('results-section'); const resultsContainer = document.getElementById('results-container'); if (!resultsSection || !resultsContainer) return; // Get current values const symbolInput = document.getElementById('symbol-input'); const timeframeSelect = document.getElementById('timeframe-select'); this.currentSymbol = symbolInput?.value?.toUpperCase() || 'BTC'; this.currentTimeframe = timeframeSelect?.value || '1h'; // Show results section resultsSection.style.display = 'block'; resultsContainer.innerHTML = `

Analyzing ${this.currentSymbol} with ${serviceId}...

`; // Scroll to results resultsSection.scrollIntoView({ behavior: 'smooth' }); try { const service = this.services.find(s => s.id === serviceId); if (!service) throw new Error('Service not found'); const url = `${service.endpoint}?symbol=${encodeURIComponent(this.currentSymbol)}&timeframe=${encodeURIComponent(this.currentTimeframe)}`; const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const result = await response.json(); this.renderResult(service, result); } catch (error) { console.error('[Services] Analysis error:', error); resultsContainer.innerHTML = `

Analysis Failed

${error.message}

`; } } async analyzeAll() { const resultsSection = document.getElementById('results-section'); const resultsContainer = document.getElementById('results-container'); if (!resultsSection || !resultsContainer) return; // Get current values const symbolInput = document.getElementById('symbol-input'); const timeframeSelect = document.getElementById('timeframe-select'); this.currentSymbol = symbolInput?.value?.toUpperCase() || 'BTC'; this.currentTimeframe = timeframeSelect?.value || '1h'; // Show loading resultsSection.style.display = 'block'; resultsContainer.innerHTML = `

Running comprehensive analysis on ${this.currentSymbol}...

`; resultsSection.scrollIntoView({ behavior: 'smooth' }); try { const url = `/api/indicators/comprehensive?symbol=${encodeURIComponent(this.currentSymbol)}&timeframe=${encodeURIComponent(this.currentTimeframe)}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json', }, }); // Handle different response scenarios let result; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { result = await response.json(); } else { throw new Error(`Unexpected response type: ${contentType || 'unknown'}`); } // Check if the result indicates an error even with 200 status if (result.success === false && result.error) { console.warn('[Services] API returned error in response:', result.error); this.showToast(`⚠️ ${result.error}`, 'warning'); } // Render even with warnings/errors, as fallback data is still useful this.renderComprehensiveResult(result); // Show warning if using fallback data if (result.source === 'fallback' || result.warning) { this.showToast('⚠️ Using fallback data - some services may be unavailable', 'warning'); } } catch (error) { console.error('[Services] Comprehensive analysis error:', error); // More detailed error message let errorMessage = 'Unable to complete analysis'; if (error.message.includes('HTTP 500')) { errorMessage = 'Server error - the analysis service is temporarily unavailable'; } else if (error.message.includes('Failed to fetch')) { errorMessage = 'Network error - please check your connection'; } else if (error.message.includes('timeout')) { errorMessage = 'Request timeout - the service took too long to respond'; } else { errorMessage = error.message; } resultsContainer.innerHTML = `

Analysis Failed

${errorMessage}

`; this.showToast(`❌ ${errorMessage}`, 'error'); } } renderResult(service, result) { const resultsContainer = document.getElementById('results-container'); if (!resultsContainer) return; const signalClass = this.getSignalClass(result.signal); const data = result.data || {}; let valuesHtml = ''; for (const [key, value] of Object.entries(data)) { if (value !== null && value !== undefined) { valuesHtml += `
${this.formatLabel(key)} ${this.formatValue(value)}
`; } } resultsContainer.innerHTML = `

${service.icon} ${service.name}

${result.signal || 'N/A'}
${valuesHtml}

${result.description || 'No description available'}

`; } renderComprehensiveResult(result) { const resultsContainer = document.getElementById('results-container'); if (!resultsContainer) return; const indicators = result.indicators || {}; const signals = result.signals || {}; let cardsHtml = ''; // Overall signal card const overallClass = this.getSignalClass(result.overall_signal?.toLowerCase()); cardsHtml += `

🎯 Overall Analysis - ${result.symbol || this.currentSymbol}

${result.overall_signal || 'N/A'}
Current Price ${this.formatValue(result.current_price)}
Confidence ${result.confidence || 0}%

Recommendation: ${result.recommendation || 'No recommendation available'}

`; // Individual indicator cards const indicatorMeta = { bollinger_bands: { icon: '📊', name: 'Bollinger Bands' }, stoch_rsi: { icon: '📈', name: 'Stochastic RSI' }, atr: { icon: '📉', name: 'ATR' }, sma: { icon: '〰️', name: 'SMA' }, ema: { icon: '📐', name: 'EMA' }, macd: { icon: '🔀', name: 'MACD' }, rsi: { icon: '💪', name: 'RSI' } }; for (const [key, data] of Object.entries(indicators)) { const meta = indicatorMeta[key] || { icon: '📊', name: key }; const signal = signals[key] || 'neutral'; const signalClass = this.getSignalClass(signal); let valuesHtml = ''; if (typeof data === 'object') { for (const [k, v] of Object.entries(data)) { if (v !== null && v !== undefined) { valuesHtml += `
${this.formatLabel(k)} ${this.formatValue(v)}
`; } } } cardsHtml += `

${meta.icon} ${meta.name}

${signal}
${valuesHtml || '

No data

'}
`; } resultsContainer.innerHTML = cardsHtml; } getSignalClass(signal) { if (!signal) return 'neutral'; const s = signal.toLowerCase(); if (s.includes('buy') || s.includes('bullish') || s.includes('oversold') || s.includes('strong_buy')) { return 'bullish'; } if (s.includes('sell') || s.includes('bearish') || s.includes('overbought') || s.includes('strong_sell')) { return 'bearish'; } return 'neutral'; } formatLabel(key) { return key .replace(/_/g, ' ') .replace(/([A-Z])/g, ' $1') .split(' ') .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) .join(' '); } formatValue(value) { if (value === null || value === undefined) return '—'; if (typeof value === 'number') { if (value > 1000000) return (value / 1000000).toFixed(2) + 'M'; if (value > 1000) return (value / 1000).toFixed(2) + 'K'; if (value < 0.0001 && value > 0) return value.toExponential(2); if (Number.isInteger(value)) return value.toLocaleString(); return value.toFixed(value < 1 ? 4 : 2); } return String(value); } updateTimestamp() { const el = document.getElementById('last-update'); if (el) { el.textContent = `Updated: ${new Date().toLocaleTimeString()}`; } } showToast(message, type = 'info') { console.log(`[Toast ${type}]`, message); // Implement toast if needed } } // Initialize const servicesPage = new ServicesPage(); servicesPage.init(); // Expose globally window.servicesPage = servicesPage; export default servicesPage;