| |
| |
| |
|
|
| 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() { |
| |
| document.getElementById('refresh-btn')?.addEventListener('click', () => { |
| this.loadServices(); |
| }); |
|
|
| |
| document.getElementById('symbol-input')?.addEventListener('change', (e) => { |
| this.currentSymbol = e.target.value.toUpperCase() || 'BTC'; |
| }); |
|
|
| |
| document.getElementById('timeframe-select')?.addEventListener('change', (e) => { |
| this.currentTimeframe = e.target.value || '1h'; |
| }); |
|
|
| |
| document.getElementById('analyze-all-btn')?.addEventListener('click', () => { |
| this.analyzeAll(); |
| }); |
|
|
| |
| 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) { |
| |
| setTimeout(() => { |
| this.analyzeService(service); |
| }, 500); |
| } |
| } |
|
|
| async loadServices() { |
| const grid = document.getElementById('services-grid'); |
| if (!grid) return; |
|
|
| grid.innerHTML = ` |
| <div class="loading-state"> |
| <div class="loading-spinner"></div> |
| <p>Loading indicator services...</p> |
| </div> |
| `; |
|
|
| 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 { |
| |
| 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 = ` |
| <div class="error-state"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <circle cx="12" cy="12" r="10"></circle> |
| <line x1="12" y1="8" x2="12" y2="12"></line> |
| <line x1="12" y1="16" x2="12.01" y2="16"></line> |
| </svg> |
| <h3>No services found</h3> |
| <p>No indicator services match the selected category.</p> |
| </div> |
| `; |
| return; |
| } |
|
|
| grid.innerHTML = filteredServices.map(service => ` |
| <div class="service-card-large" data-service="${service.id}"> |
| <div class="service-card-header"> |
| <div class="service-card-icon">${service.icon}</div> |
| <div class="service-card-title"> |
| <h3>${service.name}</h3> |
| <span class="category-tag">${service.category}</span> |
| </div> |
| </div> |
| <div class="service-card-body"> |
| <p class="service-card-desc">${service.description}</p> |
| <div class="service-card-params"> |
| ${service.parameters.map(p => `<span class="param-tag">${p}</span>`).join('')} |
| </div> |
| </div> |
| <div class="service-card-footer"> |
| <div class="service-status"> |
| <span class="status-dot"></span> |
| <span>Available</span> |
| </div> |
| <button class="btn btn-primary" onclick="servicesPage.analyzeService('${service.id}')"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> |
| </svg> |
| Analyze |
| </button> |
| </div> |
| </div> |
| `).join(''); |
| } |
|
|
| async analyzeService(serviceId) { |
| const resultsSection = document.getElementById('results-section'); |
| const resultsContainer = document.getElementById('results-container'); |
| |
| if (!resultsSection || !resultsContainer) return; |
|
|
| |
| const symbolInput = document.getElementById('symbol-input'); |
| const timeframeSelect = document.getElementById('timeframe-select'); |
| |
| this.currentSymbol = symbolInput?.value?.toUpperCase() || 'BTC'; |
| this.currentTimeframe = timeframeSelect?.value || '1h'; |
|
|
| |
| resultsSection.style.display = 'block'; |
| resultsContainer.innerHTML = ` |
| <div class="loading-state"> |
| <div class="loading-spinner"></div> |
| <p>Analyzing ${this.currentSymbol} with ${serviceId}...</p> |
| </div> |
| `; |
|
|
| |
| 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 = ` |
| <div class="error-state"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <circle cx="12" cy="12" r="10"></circle> |
| <line x1="15" y1="9" x2="9" y2="15"></line> |
| <line x1="9" y1="9" x2="15" y2="15"></line> |
| </svg> |
| <h3>Analysis Failed</h3> |
| <p>${error.message}</p> |
| <button class="btn btn-primary" onclick="servicesPage.analyzeService('${serviceId}')">Retry</button> |
| </div> |
| `; |
| } |
| } |
|
|
| async analyzeAll() { |
| const resultsSection = document.getElementById('results-section'); |
| const resultsContainer = document.getElementById('results-container'); |
| |
| if (!resultsSection || !resultsContainer) return; |
|
|
| |
| const symbolInput = document.getElementById('symbol-input'); |
| const timeframeSelect = document.getElementById('timeframe-select'); |
| |
| this.currentSymbol = symbolInput?.value?.toUpperCase() || 'BTC'; |
| this.currentTimeframe = timeframeSelect?.value || '1h'; |
|
|
| |
| resultsSection.style.display = 'block'; |
| resultsContainer.innerHTML = ` |
| <div class="loading-state"> |
| <div class="loading-spinner"></div> |
| <p>Running comprehensive analysis on ${this.currentSymbol}...</p> |
| </div> |
| `; |
|
|
| 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', |
| }, |
| }); |
|
|
| |
| 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'}`); |
| } |
|
|
| |
| if (result.success === false && result.error) { |
| console.warn('[Services] API returned error in response:', result.error); |
| this.showToast(`β οΈ ${result.error}`, 'warning'); |
| } |
|
|
| |
| this.renderComprehensiveResult(result); |
| |
| |
| 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); |
| |
| |
| 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 = ` |
| <div class="error-state"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <circle cx="12" cy="12" r="10"></circle> |
| <line x1="15" y1="9" x2="9" y2="15"></line> |
| <line x1="9" y1="9" x2="15" y2="15"></line> |
| </svg> |
| <h3>Analysis Failed</h3> |
| <p style="margin: 1rem 0;">${errorMessage}</p> |
| <div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;"> |
| <button class="btn btn-primary" onclick="servicesPage.analyzeAll()"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="23 4 23 10 17 10"></polyline> |
| <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path> |
| </svg> |
| Retry |
| </button> |
| <button class="btn btn-secondary" onclick="window.location.href='/static/pages/service-health/index.html'"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> |
| </svg> |
| Check Service Status |
| </button> |
| </div> |
| </div> |
| `; |
| |
| 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 += ` |
| <div class="result-value"> |
| <span class="label">${this.formatLabel(key)}</span> |
| <span class="value">${this.formatValue(value)}</span> |
| </div> |
| `; |
| } |
| } |
|
|
| resultsContainer.innerHTML = ` |
| <div class="result-card"> |
| <div class="result-card-header"> |
| <h4> |
| <span class="indicator-icon">${service.icon}</span> |
| ${service.name} |
| </h4> |
| <span class="signal-badge ${signalClass}">${result.signal || 'N/A'}</span> |
| </div> |
| <div class="result-card-body"> |
| <div class="result-values"> |
| ${valuesHtml} |
| </div> |
| <div class="result-description"> |
| <p>${result.description || 'No description available'}</p> |
| </div> |
| </div> |
| </div> |
| `; |
| } |
|
|
| renderComprehensiveResult(result) { |
| const resultsContainer = document.getElementById('results-container'); |
| if (!resultsContainer) return; |
|
|
| const indicators = result.indicators || {}; |
| const signals = result.signals || {}; |
|
|
| let cardsHtml = ''; |
|
|
| |
| const overallClass = this.getSignalClass(result.overall_signal?.toLowerCase()); |
| cardsHtml += ` |
| <div class="result-card" style="grid-column: 1 / -1;"> |
| <div class="result-card-header" style="background: linear-gradient(135deg, rgba(20, 184, 166, 0.2), rgba(6, 182, 212, 0.15));"> |
| <h4> |
| <span class="indicator-icon">π―</span> |
| Overall Analysis - ${result.symbol || this.currentSymbol} |
| </h4> |
| <span class="signal-badge ${overallClass}">${result.overall_signal || 'N/A'}</span> |
| </div> |
| <div class="result-card-body"> |
| <div class="result-values"> |
| <div class="result-value"> |
| <span class="label">Current Price</span> |
| <span class="value">${this.formatValue(result.current_price)}</span> |
| </div> |
| <div class="result-value"> |
| <span class="label">Confidence</span> |
| <span class="value">${result.confidence || 0}%</span> |
| </div> |
| </div> |
| <div class="result-description"> |
| <p><strong>Recommendation:</strong> ${result.recommendation || 'No recommendation available'}</p> |
| </div> |
| </div> |
| </div> |
| `; |
|
|
| |
| 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 += ` |
| <div class="result-value"> |
| <span class="label">${this.formatLabel(k)}</span> |
| <span class="value">${this.formatValue(v)}</span> |
| </div> |
| `; |
| } |
| } |
| } |
|
|
| cardsHtml += ` |
| <div class="result-card"> |
| <div class="result-card-header"> |
| <h4> |
| <span class="indicator-icon">${meta.icon}</span> |
| ${meta.name} |
| </h4> |
| <span class="signal-badge ${signalClass}">${signal}</span> |
| </div> |
| <div class="result-card-body"> |
| <div class="result-values"> |
| ${valuesHtml || '<p style="grid-column: 1/-1; text-align: center; color: var(--text-muted);">No data</p>'} |
| </div> |
| </div> |
| </div> |
| `; |
| } |
|
|
| 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); |
| |
| } |
| } |
|
|
| |
| const servicesPage = new ServicesPage(); |
| servicesPage.init(); |
|
|
| |
| window.servicesPage = servicesPage; |
|
|
| export default servicesPage; |
|
|