Files
gogo2/ANNOTATE/web/static/js/live_updates_polling.js
2025-12-10 11:58:53 +02:00

318 lines
12 KiB
JavaScript

/**
* 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': {...}, ... }
console.log('[Live Updates] Processing chart_updates:', Object.keys(data.chart_updates));
Object.entries(data.chart_updates).forEach(([timeframe, update]) => {
if (update) {
console.log(`[Live Updates] Calling onChartUpdate for ${timeframe}:`, {
symbol: update.symbol,
timeframe: update.timeframe,
timestamp: update.candle?.timestamp,
is_confirmed: update.is_confirmed
});
this.onChartUpdate(update);
} else {
console.warn(`[Live Updates] Update for ${timeframe} is null/undefined`);
}
});
} else {
if (!data.chart_updates) {
console.debug('[Live Updates] No chart_updates in response');
}
if (!this.onChartUpdate) {
console.warn('[Live Updates] onChartUpdate callback not set!');
}
}
// 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 = '<span class="badge bg-success">Live</span>';
} else {
statusElement.innerHTML = '<span class="badge bg-secondary">Offline</span>';
}
}
};
window.liveUpdatesPolling.onChartUpdate = function(data) {
// Update chart with new candle
// data structure: { symbol, timeframe, candle: {...}, is_confirmed: true/false }
console.log('[onChartUpdate] Callback invoked with data:', {
symbol: data.symbol,
timeframe: data.timeframe,
hasCandle: !!data.candle,
is_confirmed: data.is_confirmed,
hasAppState: !!window.appState,
hasChartManager: !!(window.appState && window.appState.chartManager)
});
if (window.appState && window.appState.chartManager) {
// Pass the full update object so is_confirmed is available
const candleWithFlag = {
...data.candle,
is_confirmed: data.is_confirmed
};
console.log('[onChartUpdate] Calling updateLatestCandle with:', {
symbol: data.symbol,
timeframe: data.timeframe,
candle: candleWithFlag
});
window.appState.chartManager.updateLatestCandle(data.symbol, data.timeframe, candleWithFlag);
} else {
console.warn('[onChartUpdate] Chart manager not available!', {
hasAppState: !!window.appState,
hasChartManager: !!(window.appState && window.appState.chartManager)
});
}
};
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();
}
});