Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
| /** | |
| * Enhanced Market Monitor Agent V2 | |
| * Real-time market monitoring with WebSocket support | |
| * Features: Multi-exchange, error recovery, notification system | |
| */ | |
| /** | |
| * Enhanced Market Monitor Agent | |
| */ | |
| export class EnhancedMarketMonitor { | |
| constructor(config = {}) { | |
| this.symbol = config.symbol || 'BTC'; | |
| this.strategy = config.strategy || 'ict-market-structure'; | |
| this.interval = config.interval || 60000; | |
| this.useWebSocket = config.useWebSocket !== false; | |
| this.isRunning = false; | |
| this.intervalId = null; | |
| this.wsConnection = null; | |
| this.reconnectAttempts = 0; | |
| this.maxReconnectAttempts = 10; | |
| this.lastSignal = null; | |
| this.lastPrice = null; | |
| this.priceHistory = []; | |
| this.maxHistoryLength = 200; | |
| this.callbacks = { | |
| onSignal: null, | |
| onError: null, | |
| onPriceUpdate: null, | |
| onConnectionChange: null | |
| }; | |
| this.errorCount = 0; | |
| this.maxErrors = 5; | |
| this.circuitBreakerOpen = false; | |
| this.lastAnalysisTime = 0; | |
| this.minAnalysisInterval = 10000; | |
| this.exchanges = ['binance', 'coinbase', 'kraken']; | |
| this.currentExchange = 'binance'; | |
| this.failedExchanges = new Set(); | |
| } | |
| /** | |
| * Start monitoring with automatic fallback | |
| */ | |
| async start() { | |
| if (this.isRunning) { | |
| console.warn('[EnhancedMonitor] Already running'); | |
| return { success: false, message: 'Already running' }; | |
| } | |
| console.log(`[EnhancedMonitor] Starting for ${this.symbol} with ${this.strategy}`); | |
| this.isRunning = true; | |
| this.circuitBreakerOpen = false; | |
| this.errorCount = 0; | |
| try { | |
| // Try WebSocket first | |
| if (this.useWebSocket) { | |
| await this.connectWebSocket(); | |
| } | |
| // Start polling as fallback/supplement | |
| await this.startPolling(); | |
| // Emit connection status | |
| this.emitConnectionChange('connected'); | |
| return { success: true, message: 'Monitor started successfully' }; | |
| } catch (error) { | |
| console.error('[EnhancedMonitor] Start error:', error); | |
| this.emitError(error); | |
| return { success: false, message: error.message }; | |
| } | |
| } | |
| /** | |
| * Stop monitoring | |
| */ | |
| stop() { | |
| if (!this.isRunning) return; | |
| console.log('[EnhancedMonitor] Stopping...'); | |
| this.isRunning = false; | |
| // Stop polling | |
| if (this.intervalId) { | |
| clearInterval(this.intervalId); | |
| this.intervalId = null; | |
| } | |
| // Close WebSocket | |
| if (this.wsConnection) { | |
| this.wsConnection.close(); | |
| this.wsConnection = null; | |
| } | |
| this.emitConnectionChange('disconnected'); | |
| } | |
| /** | |
| * Connect to WebSocket for real-time updates | |
| */ | |
| async connectWebSocket() { | |
| const wsUrl = this.getWebSocketUrl(this.currentExchange); | |
| if (!wsUrl) { | |
| console.warn('[EnhancedMonitor] WebSocket not available for current exchange'); | |
| return; | |
| } | |
| try { | |
| this.wsConnection = new WebSocket(wsUrl); | |
| this.wsConnection.onopen = () => { | |
| console.log('[EnhancedMonitor] WebSocket connected'); | |
| this.reconnectAttempts = 0; | |
| this.emitConnectionChange('websocket-connected'); | |
| // Subscribe to symbol | |
| this.subscribeToSymbol(); | |
| }; | |
| this.wsConnection.onmessage = (event) => { | |
| try { | |
| const data = JSON.parse(event.data); | |
| this.handleWebSocketMessage(data); | |
| } catch (error) { | |
| console.error('[EnhancedMonitor] WebSocket message error:', error); | |
| } | |
| }; | |
| this.wsConnection.onerror = (error) => { | |
| console.error('[EnhancedMonitor] WebSocket error:', error); | |
| this.handleConnectionError(error); | |
| }; | |
| this.wsConnection.onclose = () => { | |
| console.log('[EnhancedMonitor] WebSocket closed'); | |
| if (this.isRunning && this.reconnectAttempts < this.maxReconnectAttempts) { | |
| this.reconnectAttempts++; | |
| setTimeout(() => { | |
| console.log(`[EnhancedMonitor] Reconnecting... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`); | |
| this.connectWebSocket(); | |
| }, Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000)); | |
| } | |
| }; | |
| } catch (error) { | |
| console.error('[EnhancedMonitor] WebSocket connection failed:', error); | |
| this.handleConnectionError(error); | |
| } | |
| } | |
| /** | |
| * Get WebSocket URL for exchange | |
| */ | |
| getWebSocketUrl(exchange) { | |
| const symbol = this.symbol.toLowerCase(); | |
| const urls = { | |
| binance: `wss://stream.binance.com:9443/ws/${symbol}usdt@kline_1m`, | |
| coinbase: `wss://ws-feed.exchange.coinbase.com`, | |
| kraken: `wss://ws.kraken.com` | |
| }; | |
| return urls[exchange]; | |
| } | |
| /** | |
| * Subscribe to symbol on WebSocket | |
| */ | |
| subscribeToSymbol() { | |
| if (!this.wsConnection || this.wsConnection.readyState !== WebSocket.OPEN) { | |
| return; | |
| } | |
| const symbol = this.symbol.toUpperCase(); | |
| // Exchange-specific subscription | |
| if (this.currentExchange === 'coinbase') { | |
| this.wsConnection.send(JSON.stringify({ | |
| type: 'subscribe', | |
| channels: [{ name: 'ticker', product_ids: [`${symbol}-USD`] }] | |
| })); | |
| } else if (this.currentExchange === 'kraken') { | |
| this.wsConnection.send(JSON.stringify({ | |
| event: 'subscribe', | |
| pair: [`${symbol}/USD`], | |
| subscription: { name: 'ticker' } | |
| })); | |
| } | |
| // Binance doesn't need explicit subscription in URL | |
| } | |
| /** | |
| * Handle WebSocket messages | |
| */ | |
| handleWebSocketMessage(data) { | |
| try { | |
| const priceData = this.parseWebSocketData(data); | |
| if (priceData) { | |
| this.lastPrice = priceData.price; | |
| this.addToPriceHistory(priceData); | |
| this.emitPriceUpdate(priceData); | |
| // Throttled analysis | |
| const now = Date.now(); | |
| if (now - this.lastAnalysisTime >= this.minAnalysisInterval) { | |
| this.lastAnalysisTime = now; | |
| this.performAnalysis(); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('[EnhancedMonitor] Message parsing error:', error); | |
| } | |
| } | |
| /** | |
| * Parse WebSocket data from different exchanges | |
| */ | |
| parseWebSocketData(data) { | |
| try { | |
| // Binance format | |
| if (data.e === 'kline') { | |
| const kline = data.k; | |
| return { | |
| timestamp: kline.t, | |
| open: parseFloat(kline.o), | |
| high: parseFloat(kline.h), | |
| low: parseFloat(kline.l), | |
| close: parseFloat(kline.c), | |
| volume: parseFloat(kline.v), | |
| price: parseFloat(kline.c), | |
| exchange: 'binance' | |
| }; | |
| } | |
| // Coinbase format | |
| if (data.type === 'ticker') { | |
| return { | |
| timestamp: Date.now(), | |
| price: parseFloat(data.price), | |
| volume: parseFloat(data.volume_24h || 0), | |
| exchange: 'coinbase' | |
| }; | |
| } | |
| // Kraken format | |
| if (Array.isArray(data) && data[2] === 'ticker') { | |
| const ticker = data[1]; | |
| return { | |
| timestamp: Date.now(), | |
| price: parseFloat(ticker.c[0]), | |
| volume: parseFloat(ticker.v[1]), | |
| exchange: 'kraken' | |
| }; | |
| } | |
| return null; | |
| } catch (error) { | |
| console.error('[EnhancedMonitor] Data parsing error:', error); | |
| return null; | |
| } | |
| } | |
| /** | |
| * Add price to history | |
| */ | |
| addToPriceHistory(priceData) { | |
| this.priceHistory.push(priceData); | |
| // Keep history at max length | |
| if (this.priceHistory.length > this.maxHistoryLength) { | |
| this.priceHistory.shift(); | |
| } | |
| } | |
| /** | |
| * Start polling as fallback | |
| */ | |
| async startPolling() { | |
| // Initial check | |
| await this.checkMarket(); | |
| // Set up interval | |
| this.intervalId = setInterval(async () => { | |
| if (!this.circuitBreakerOpen) { | |
| await this.checkMarket(); | |
| } else { | |
| this.attemptCircuitBreakerReset(); | |
| } | |
| }, this.interval); | |
| } | |
| /** | |
| * Check market conditions | |
| */ | |
| async checkMarket() { | |
| try { | |
| const marketData = await this.fetchMarketDataWithFallback(); | |
| if (!marketData) { | |
| throw new Error('Failed to fetch market data from all sources'); | |
| } | |
| this.resetErrorCount(); | |
| // Perform analysis | |
| await this.performAnalysis(marketData); | |
| } catch (error) { | |
| console.error('[EnhancedMonitor] Market check error:', error); | |
| this.handleError(error); | |
| } | |
| } | |
| /** | |
| * Fetch market data with multi-exchange fallback | |
| */ | |
| async fetchMarketDataWithFallback() { | |
| const availableExchanges = this.exchanges.filter(ex => !this.failedExchanges.has(ex)); | |
| if (availableExchanges.length === 0) { | |
| console.warn('[EnhancedMonitor] All exchanges failed, resetting...'); | |
| this.failedExchanges.clear(); | |
| return this.getFallbackData(); | |
| } | |
| for (const exchange of availableExchanges) { | |
| try { | |
| const data = await this.fetchFromExchange(exchange); | |
| this.currentExchange = exchange; | |
| return data; | |
| } catch (error) { | |
| console.warn(`[EnhancedMonitor] ${exchange} failed:`, error.message); | |
| this.failedExchanges.add(exchange); | |
| } | |
| } | |
| return this.getFallbackData(); | |
| } | |
| /** | |
| * Fetch from specific exchange | |
| */ | |
| async fetchFromExchange(exchange) { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 10000); | |
| try { | |
| let url; | |
| const symbol = this.symbol.toUpperCase(); | |
| switch (exchange) { | |
| case 'binance': | |
| url = `https://api.binance.com/api/v3/klines?symbol=${symbol}USDT&interval=1h&limit=100`; | |
| break; | |
| case 'coinbase': | |
| url = `https://api.exchange.coinbase.com/products/${symbol}-USD/candles?granularity=3600`; | |
| break; | |
| case 'kraken': | |
| url = `https://api.kraken.com/0/public/OHLC?pair=${symbol}USD&interval=60`; | |
| break; | |
| default: | |
| throw new Error(`Unknown exchange: ${exchange}`); | |
| } | |
| const response = await fetch(url, { | |
| signal: controller.signal, | |
| headers: { 'Accept': 'application/json' } | |
| }); | |
| clearTimeout(timeout); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| return this.normalizeExchangeData(data, exchange); | |
| } catch (error) { | |
| clearTimeout(timeout); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Normalize data from different exchanges | |
| */ | |
| normalizeExchangeData(data, exchange) { | |
| try { | |
| if (!data || typeof data !== 'object') { | |
| throw new Error('Invalid data format'); | |
| } | |
| let normalized = []; | |
| let rawData = []; | |
| // Extract data array based on exchange format | |
| switch (exchange) { | |
| case 'binance': | |
| rawData = Array.isArray(data) ? data : []; | |
| break; | |
| case 'coinbase': | |
| rawData = Array.isArray(data) ? data : []; | |
| break; | |
| case 'kraken': | |
| rawData = (data.result && typeof data.result === 'object') | |
| ? Object.values(data.result)[0] || [] | |
| : []; | |
| break; | |
| default: | |
| throw new Error(`Unknown exchange: ${exchange}`); | |
| } | |
| if (!Array.isArray(rawData) || rawData.length === 0) { | |
| throw new Error('Empty or invalid data array'); | |
| } | |
| switch (exchange) { | |
| case 'binance': | |
| normalized = rawData | |
| .filter(item => Array.isArray(item) && item.length >= 6) | |
| .map(item => { | |
| const open = parseFloat(item[1]); | |
| const high = parseFloat(item[2]); | |
| const low = parseFloat(item[3]); | |
| const close = parseFloat(item[4]); | |
| const volume = parseFloat(item[5]); | |
| // Validate OHLC | |
| if (isNaN(open) || isNaN(high) || isNaN(low) || isNaN(close) || | |
| open <= 0 || high <= 0 || low <= 0 || close <= 0 || | |
| high < low || high < Math.max(open, close) || low > Math.min(open, close)) { | |
| return null; | |
| } | |
| return { | |
| timestamp: parseInt(item[0]) || Date.now(), | |
| open: open, | |
| high: high, | |
| low: low, | |
| close: close, | |
| volume: isNaN(volume) ? 0 : volume | |
| }; | |
| }) | |
| .filter(item => item !== null); | |
| break; | |
| case 'coinbase': | |
| normalized = rawData | |
| .filter(item => Array.isArray(item) && item.length >= 5) | |
| .map(item => { | |
| const timestamp = parseInt(item[0]) * 1000; | |
| const low = parseFloat(item[1]); | |
| const high = parseFloat(item[2]); | |
| const open = parseFloat(item[3]); | |
| const close = parseFloat(item[4]); | |
| // Validate OHLC | |
| if (isNaN(open) || isNaN(high) || isNaN(low) || isNaN(close) || | |
| open <= 0 || high <= 0 || low <= 0 || close <= 0 || | |
| high < low || high < Math.max(open, close) || low > Math.min(open, close)) { | |
| return null; | |
| } | |
| return { | |
| timestamp: timestamp || Date.now(), | |
| low: low, | |
| high: high, | |
| open: open, | |
| close: close, | |
| volume: parseFloat(item[5]) || 0 | |
| }; | |
| }) | |
| .filter(item => item !== null); | |
| break; | |
| case 'kraken': | |
| normalized = rawData | |
| .filter(item => Array.isArray(item) && item.length >= 7) | |
| .map(item => { | |
| const timestamp = parseInt(item[0]) * 1000; | |
| const open = parseFloat(item[2]); | |
| const high = parseFloat(item[3]); | |
| const low = parseFloat(item[4]); | |
| const close = parseFloat(item[5]); | |
| const volume = parseFloat(item[6]); | |
| // Validate OHLC | |
| if (isNaN(open) || isNaN(high) || isNaN(low) || isNaN(close) || | |
| open <= 0 || high <= 0 || low <= 0 || close <= 0 || | |
| high < low || high < Math.max(open, close) || low > Math.min(open, close)) { | |
| return null; | |
| } | |
| return { | |
| timestamp: timestamp || Date.now(), | |
| open: open, | |
| high: high, | |
| low: low, | |
| close: close, | |
| volume: isNaN(volume) ? 0 : volume | |
| }; | |
| }) | |
| .filter(item => item !== null); | |
| break; | |
| } | |
| if (normalized.length === 0) { | |
| throw new Error('No valid data after normalization'); | |
| } | |
| return normalized.sort((a, b) => a.timestamp - b.timestamp); | |
| } catch (error) { | |
| console.error(`[EnhancedMonitor] Normalization error for ${exchange}:`, error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Get fallback demo data | |
| */ | |
| getFallbackData() { | |
| console.warn('[EnhancedMonitor] Using fallback demo data'); | |
| const data = []; | |
| const now = Date.now(); | |
| let basePrice = 50000; | |
| for (let i = 99; i >= 0; i--) { | |
| const timestamp = now - (i * 3600000); | |
| const volatility = basePrice * 0.02; | |
| const open = basePrice + (Math.random() - 0.5) * volatility; | |
| const close = open + (Math.random() - 0.5) * volatility; | |
| const high = Math.max(open, close) + Math.random() * volatility * 0.5; | |
| const low = Math.min(open, close) - Math.random() * volatility * 0.5; | |
| const volume = Math.random() * 1000000; | |
| data.push({ timestamp, open, high, low, close, volume }); | |
| basePrice = close; | |
| } | |
| return data; | |
| } | |
| /** | |
| * Perform trading analysis | |
| */ | |
| async performAnalysis(marketData = null) { | |
| try { | |
| // Use provided data or price history | |
| const ohlcvData = marketData || this.convertPriceHistoryToOHLCV(); | |
| if (!ohlcvData || ohlcvData.length < 50) { | |
| console.warn('[EnhancedMonitor] Insufficient data for analysis'); | |
| return; | |
| } | |
| // Import strategy module dynamically | |
| const { analyzeWithAdvancedStrategy } = await import('./advanced-strategies-v2.js'); | |
| const analysis = await analyzeWithAdvancedStrategy( | |
| this.symbol, | |
| this.strategy, | |
| ohlcvData | |
| ); | |
| if (this.shouldNotify(analysis)) { | |
| this.emitSignal(analysis); | |
| } | |
| } catch (error) { | |
| console.error('[EnhancedMonitor] Analysis error:', error); | |
| this.handleError(error); | |
| } | |
| } | |
| /** | |
| * Convert price history to OHLCV format | |
| */ | |
| convertPriceHistoryToOHLCV() { | |
| if (this.priceHistory.length < 10) return null; | |
| // Group by minute intervals | |
| const grouped = new Map(); | |
| this.priceHistory.forEach(item => { | |
| const minute = Math.floor(item.timestamp / 60000) * 60000; | |
| if (!grouped.has(minute)) { | |
| grouped.set(minute, { | |
| timestamp: minute, | |
| open: item.price, | |
| high: item.price, | |
| low: item.price, | |
| close: item.price, | |
| volume: item.volume || 0 | |
| }); | |
| } else { | |
| const candle = grouped.get(minute); | |
| candle.high = Math.max(candle.high, item.price); | |
| candle.low = Math.min(candle.low, item.price); | |
| candle.close = item.price; | |
| candle.volume += item.volume || 0; | |
| } | |
| }); | |
| return Array.from(grouped.values()).sort((a, b) => a.timestamp - b.timestamp); | |
| } | |
| /** | |
| * Determine if notification should be sent | |
| */ | |
| shouldNotify(analysis) { | |
| if (!analysis) return false; | |
| // Always notify on new signal type | |
| if (!this.lastSignal || this.lastSignal.signal !== analysis.signal) { | |
| this.lastSignal = analysis; | |
| return true; | |
| } | |
| // Notify on high confidence signals | |
| if (analysis.confidence >= 85 && analysis.signal !== 'hold') { | |
| return true; | |
| } | |
| // Notify on significant price moves | |
| if (this.lastPrice && analysis.entry) { | |
| const priceChange = Math.abs((analysis.entry - this.lastPrice) / this.lastPrice); | |
| if (priceChange > 0.03) { // 3% move | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Handle connection errors with fallback | |
| */ | |
| handleConnectionError(error) { | |
| this.errorCount++; | |
| if (this.errorCount >= this.maxErrors) { | |
| console.error('[EnhancedMonitor] Circuit breaker opened due to repeated errors'); | |
| this.circuitBreakerOpen = true; | |
| this.emitConnectionChange('circuit-breaker-open'); | |
| } | |
| // Try switching exchange | |
| const currentIndex = this.exchanges.indexOf(this.currentExchange); | |
| const nextIndex = (currentIndex + 1) % this.exchanges.length; | |
| this.currentExchange = this.exchanges[nextIndex]; | |
| console.log(`[EnhancedMonitor] Switching to ${this.currentExchange}`); | |
| } | |
| /** | |
| * Handle general errors | |
| */ | |
| handleError(error) { | |
| this.errorCount++; | |
| if (this.errorCount >= this.maxErrors && !this.circuitBreakerOpen) { | |
| console.error('[EnhancedMonitor] Circuit breaker triggered'); | |
| this.circuitBreakerOpen = true; | |
| this.emitConnectionChange('circuit-breaker-open'); | |
| } | |
| this.emitError(error); | |
| } | |
| /** | |
| * Reset error count on successful operations | |
| */ | |
| resetErrorCount() { | |
| if (this.errorCount > 0) { | |
| this.errorCount = Math.max(0, this.errorCount - 1); | |
| } | |
| } | |
| /** | |
| * Attempt to reset circuit breaker | |
| */ | |
| attemptCircuitBreakerReset() { | |
| const resetTime = 60000; // 1 minute | |
| if (this.errorCount > 0) { | |
| this.errorCount--; | |
| } | |
| if (this.errorCount === 0) { | |
| console.log('[EnhancedMonitor] Circuit breaker reset, resuming...'); | |
| this.circuitBreakerOpen = false; | |
| this.failedExchanges.clear(); | |
| this.emitConnectionChange('circuit-breaker-reset'); | |
| } | |
| } | |
| /** | |
| * Emit signal event | |
| */ | |
| emitSignal(analysis) { | |
| console.log('[EnhancedMonitor] Signal:', analysis); | |
| if (this.callbacks.onSignal) { | |
| this.callbacks.onSignal(analysis); | |
| } | |
| } | |
| /** | |
| * Emit price update event | |
| */ | |
| emitPriceUpdate(priceData) { | |
| if (this.callbacks.onPriceUpdate) { | |
| this.callbacks.onPriceUpdate(priceData); | |
| } | |
| } | |
| /** | |
| * Emit error event | |
| */ | |
| emitError(error) { | |
| if (this.callbacks.onError) { | |
| this.callbacks.onError(error); | |
| } | |
| } | |
| /** | |
| * Emit connection change event | |
| */ | |
| emitConnectionChange(status) { | |
| console.log('[EnhancedMonitor] Connection status:', status); | |
| if (this.callbacks.onConnectionChange) { | |
| this.callbacks.onConnectionChange({ | |
| status, | |
| exchange: this.currentExchange, | |
| websocket: !!this.wsConnection, | |
| circuitBreaker: this.circuitBreakerOpen | |
| }); | |
| } | |
| } | |
| /** | |
| * Set callback functions | |
| */ | |
| on(event, callback) { | |
| if (this.callbacks.hasOwnProperty(`on${event.charAt(0).toUpperCase()}${event.slice(1)}`)) { | |
| this.callbacks[`on${event.charAt(0).toUpperCase()}${event.slice(1)}`] = callback; | |
| } | |
| } | |
| /** | |
| * Update configuration | |
| */ | |
| updateConfig(config) { | |
| let needsRestart = false; | |
| if (config.symbol && config.symbol !== this.symbol) { | |
| this.symbol = config.symbol; | |
| needsRestart = true; | |
| } | |
| if (config.strategy) { | |
| this.strategy = config.strategy; | |
| } | |
| if (config.interval) { | |
| this.interval = config.interval; | |
| needsRestart = true; | |
| } | |
| if (needsRestart && this.isRunning) { | |
| this.stop(); | |
| this.start(); | |
| } | |
| } | |
| /** | |
| * Get current status | |
| */ | |
| getStatus() { | |
| return { | |
| isRunning: this.isRunning, | |
| symbol: this.symbol, | |
| strategy: this.strategy, | |
| interval: this.interval, | |
| exchange: this.currentExchange, | |
| websocketConnected: !!(this.wsConnection && this.wsConnection.readyState === WebSocket.OPEN), | |
| circuitBreakerOpen: this.circuitBreakerOpen, | |
| errorCount: this.errorCount, | |
| lastSignal: this.lastSignal, | |
| lastPrice: this.lastPrice, | |
| historyLength: this.priceHistory.length, | |
| failedExchanges: Array.from(this.failedExchanges) | |
| }; | |
| } | |
| } | |
| export default EnhancedMarketMonitor; | |