/** * Polling-based Live Updates for ANNOTATE * Replaces WebSocket with simple polling (like clean_dashboard) */ class LiveUpdatesPolling { constructor() { this.pollInterval = null; this.pollDelay = 2000; // Poll every 2 seconds (like clean_dashboard) this.subscriptions = new Set(); this.isPolling = false; // Callbacks this.onChartUpdate = null; this.onPredictionUpdate = null; this.onConnectionChange = null; console.log('LiveUpdatesPolling initialized'); } start() { if (this.isPolling) { console.log('Already polling'); return; } this.isPolling = true; this._startPolling(); if (this.onConnectionChange) { this.onConnectionChange(true); } console.log('Started polling for live updates'); } stop() { if (this.pollInterval) { clearInterval(this.pollInterval); this.pollInterval = null; } this.isPolling = false; if (this.onConnectionChange) { this.onConnectionChange(false); } console.log('Stopped polling for live updates'); } _startPolling() { // Poll immediately, then set interval this._poll(); this.pollInterval = setInterval(() => { this._poll(); }, this.pollDelay); } _poll() { // OPTIMIZATION: Batch all subscriptions into a single API call // Group by symbol to reduce API calls from 4 to 1 const symbolGroups = {}; this.subscriptions.forEach(sub => { if (!symbolGroups[sub.symbol]) { symbolGroups[sub.symbol] = []; } symbolGroups[sub.symbol].push(sub.timeframe); }); // Make one call per symbol with all timeframes Object.entries(symbolGroups).forEach(([symbol, timeframes]) => { fetch('/api/live-updates-batch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol: symbol, timeframes: timeframes }) }) .then(response => response.json()) .then(data => { if (data.success) { // Handle chart updates for each timeframe if (data.chart_updates && this.onChartUpdate) { // chart_updates is an object: { '1s': {...}, '1m': {...}, ... } Object.entries(data.chart_updates).forEach(([timeframe, update]) => { if (update) { this.onChartUpdate(update); } }); } // Handle prediction update (single prediction for all timeframes) // data.prediction is in format { transformer: {...}, dqn: {...}, cnn: {...} } if (data.prediction && this.onPredictionUpdate) { console.log('[Live Updates] Received prediction data:', { has_transformer: !!data.prediction.transformer, has_dqn: !!data.prediction.dqn, has_cnn: !!data.prediction.cnn, transformer_action: data.prediction.transformer?.action, transformer_confidence: data.prediction.transformer?.confidence, has_predicted_candle: !!data.prediction.transformer?.predicted_candle }); this.onPredictionUpdate(data.prediction); } } else { console.debug('[Live Updates] Response not successful:', data); } }) .catch(error => { console.debug('Polling error:', error); }); }); } subscribe(symbol, timeframe) { const key = `${symbol}_${timeframe}`; this.subscriptions.add({ symbol, timeframe, key }); // Auto-start polling if not already started if (!this.isPolling) { this.start(); } console.log(`Subscribed to live updates: ${symbol} ${timeframe}`); } unsubscribe(symbol, timeframe) { const key = `${symbol}_${timeframe}`; this.subscriptions.forEach(sub => { if (sub.key === key) { this.subscriptions.delete(sub); } }); // Stop polling if no subscriptions if (this.subscriptions.size === 0) { this.stop(); } console.log(`Unsubscribed from live updates: ${symbol} ${timeframe}`); } isConnected() { return this.isPolling; } } // Global instance window.liveUpdatesPolling = null; // Initialize on page load document.addEventListener('DOMContentLoaded', function() { // Initialize polling window.liveUpdatesPolling = new LiveUpdatesPolling(); // Setup callbacks (same interface as WebSocket version) window.liveUpdatesPolling.onConnectionChange = function(connected) { const statusElement = document.getElementById('ws-connection-status'); if (statusElement) { if (connected) { statusElement.innerHTML = 'Live'; } else { statusElement.innerHTML = 'Offline'; } } }; window.liveUpdatesPolling.onChartUpdate = function(data) { // Update chart with new candle if (window.appState && window.appState.chartManager) { window.appState.chartManager.updateLatestCandle(data.symbol, data.timeframe, data.candle); } }; window.liveUpdatesPolling.onPredictionUpdate = function(data) { // CRITICAL FIX: data is already in format { transformer: {...}, dqn: {...}, cnn: {...} } console.log('[Live Updates] Prediction received:', data); // Update prediction visualization on charts if (window.appState && window.appState.chartManager) { // Store predictions for later use if (!window.appState.chartManager.predictions) { window.appState.chartManager.predictions = {}; } // Update stored predictions if (data.transformer) { window.appState.chartManager.predictions['transformer'] = data.transformer; } if (data.dqn) { window.appState.chartManager.predictions['dqn'] = data.dqn; } if (data.cnn) { window.appState.chartManager.predictions['cnn'] = data.cnn; } // Update charts with predictions window.appState.chartManager.updatePredictions(data); } // Update prediction display in UI if (typeof updatePredictionDisplay === 'function') { // updatePredictionDisplay expects a single prediction object, not the full data structure // Pass the transformer prediction if available if (data.transformer) { updatePredictionDisplay(data.transformer); } } // Add to prediction history (use transformer prediction if available) if (typeof predictionHistory !== 'undefined') { const predictionToAdd = data.transformer || data.dqn || data.cnn || data; if (predictionToAdd) { predictionHistory.unshift(predictionToAdd); if (predictionHistory.length > 5) { predictionHistory = predictionHistory.slice(0, 5); } if (typeof updatePredictionHistory === 'function') { updatePredictionHistory(); } } } }; // Function to subscribe to all active timeframes function subscribeToActiveTimeframes() { if (window.appState && window.appState.currentSymbol && window.appState.currentTimeframes) { const symbol = window.appState.currentSymbol; window.appState.currentTimeframes.forEach(timeframe => { window.liveUpdatesPolling.subscribe(symbol, timeframe); }); console.log(`Subscribed to live updates for ${symbol}: ${window.appState.currentTimeframes.join(', ')}`); } } // Auto-start polling console.log('Auto-starting polling for live updates...'); window.liveUpdatesPolling.start(); // Wait for DOM and appState to be ready, then subscribe function initializeSubscriptions() { // Wait a bit for charts to initialize setTimeout(() => { subscribeToActiveTimeframes(); }, 2000); } // Subscribe when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeSubscriptions); } else { initializeSubscriptions(); } // Also monitor for appState changes (fallback) let lastTimeframes = null; setInterval(() => { if (window.appState && window.appState.currentTimeframes && window.appState.chartManager) { const currentTimeframes = window.appState.currentTimeframes.join(','); if (currentTimeframes !== lastTimeframes) { lastTimeframes = currentTimeframes; subscribeToActiveTimeframes(); } } }, 3000); }); // Cleanup on page unload window.addEventListener('beforeunload', function() { if (window.liveUpdatesPolling) { window.liveUpdatesPolling.stop(); } });