diff --git a/ANNOTATE/web/app.py b/ANNOTATE/web/app.py index eb7233a..3f0d9be 100644 --- a/ANNOTATE/web/app.py +++ b/ANNOTATE/web/app.py @@ -2525,7 +2525,17 @@ class AnnotationDashboard: if transformer_preds: # Convert any remaining tensors to Python types before JSON serialization transformer_pred = transformer_preds[-1].copy() + + # CRITICAL: Log prediction structure to debug missing predicted_candle + logger.debug(f"Transformer prediction keys: {list(transformer_pred.keys())}") + if 'predicted_candle' in transformer_pred: + logger.debug(f"predicted_candle timeframes: {list(transformer_pred['predicted_candle'].keys()) if isinstance(transformer_pred['predicted_candle'], dict) else 'not a dict'}") + predictions['transformer'] = self._serialize_prediction(transformer_pred) + + # Verify predicted_candle is preserved after serialization + if 'predicted_candle' not in predictions['transformer'] and 'predicted_candle' in transformer_pred: + logger.warning("predicted_candle was lost during serialization!") if predictions: response['prediction'] = predictions diff --git a/ANNOTATE/web/static/js/chart_manager.js b/ANNOTATE/web/static/js/chart_manager.js index 9f3be71..cd143c2 100644 --- a/ANNOTATE/web/static/js/chart_manager.js +++ b/ANNOTATE/web/static/js/chart_manager.js @@ -1021,7 +1021,8 @@ class ChartManager { return close >= data.open[i] ? '#10b981' : '#ef4444'; }); - // Prepare chart data + // CRITICAL: Prepare chart data with correct yaxis assignments + // Candlestick uses 'y' (price axis on top), Volume uses 'y2' (volume axis at bottom) const chartData = [ { x: data.timestamps, @@ -1031,6 +1032,7 @@ class ChartManager { close: data.close, type: 'candlestick', name: 'Price', + yaxis: 'y', // Explicitly set to price axis (top) increasing: { line: { color: '#10b981', width: 1 }, fillcolor: '#10b981' @@ -1044,7 +1046,7 @@ class ChartManager { x: data.timestamps, y: data.volume, type: 'bar', - yaxis: 'y2', + yaxis: 'y2', // Explicitly set to volume axis (bottom) name: 'Volume', marker: { color: volumeColors, @@ -1183,16 +1185,109 @@ class ChartManager { } } - // Use Plotly.react for efficient updates - const update = { + // CRITICAL FIX: Preserve existing layout (theme, yaxis domains, etc.) when updating + const chart = this.charts[timeframe]; + if (!chart) return; + + const plotElement = document.getElementById(plotId); + if (!plotElement || !plotElement._fullLayout) { + // Chart not initialized yet, skip + return; + } + + // Get current layout to preserve theme and settings + const currentLayout = plotElement._fullLayout; + const currentConfig = plotElement._fullConfig || {}; + + // Preserve critical layout settings + const layoutUpdate = { shapes: shapes, - annotations: annotations + annotations: annotations, + // Preserve theme colors + plot_bgcolor: currentLayout.plot_bgcolor || '#1f2937', + paper_bgcolor: currentLayout.paper_bgcolor || '#1f2937', + font: currentLayout.font || { color: '#f8f9fa', size: 11 }, + // CRITICAL: Preserve yaxis domains (price on top [0.3, 1], volume at bottom [0, 0.25]) + // This ensures charts don't get swapped + yaxis: { + domain: currentLayout.yaxis?.domain || [0.3, 1], // Price chart on top + title: currentLayout.yaxis?.title || { text: 'Price', font: { size: 10 } }, + gridcolor: currentLayout.yaxis?.gridcolor || '#374151', + color: currentLayout.yaxis?.color || '#9ca3af', + side: currentLayout.yaxis?.side || 'left' + }, + yaxis2: { + domain: currentLayout.yaxis2?.domain || [0, 0.25], // Volume chart at bottom + title: currentLayout.yaxis2?.title || { text: 'Volume', font: { size: 10 } }, + gridcolor: currentLayout.yaxis2?.gridcolor || '#374151', + color: currentLayout.yaxis2?.color || '#9ca3af', + showgrid: currentLayout.yaxis2?.showgrid !== undefined ? currentLayout.yaxis2.showgrid : false, + side: currentLayout.yaxis2?.side || 'right' + }, + // Preserve xaxis settings + xaxis: { + gridcolor: currentLayout.xaxis?.gridcolor || '#374151', + color: currentLayout.xaxis?.color || '#9ca3af' + } }; - Plotly.react(plotId, chartData, update); + + // Use Plotly.react with full layout to preserve theme and structure + Plotly.react(plotId, chartData, layoutUpdate, currentConfig).then(() => { + // Restore predictions and signals after chart update + if (this.predictions && this.predictions[timeframe]) { + this.updatePredictions({ [timeframe]: this.predictions[timeframe] }); + } + + // Restore ghost candles if they exist + if (this.ghostCandleHistory && this.ghostCandleHistory[timeframe]) { + this.ghostCandleHistory[timeframe].forEach(ghost => { + this._addGhostCandle(timeframe, ghost); + }); + } + + // CRITICAL: Fetch latest predictions from API after refresh + // This ensures predictions and signals are displayed even after refresh + this._fetchAndRestorePredictions(timeframe); + }); } }); } + /** + * Fetch and restore predictions after chart refresh + */ + _fetchAndRestorePredictions(timeframe) { + try { + // Fetch latest signals which include predictions + fetch('/api/realtime-inference/signals') + .then(response => response.json()) + .then(data => { + if (data.success && data.signals && data.signals.length > 0) { + const latest = data.signals[0]; + + // Update predictions if transformer prediction exists + if (latest.predicted_candle && Object.keys(latest.predicted_candle).length > 0) { + const predictions = {}; + predictions['transformer'] = latest; + this.updatePredictions(predictions); + } + + // Update signals on chart + if (latest.action && ['BUY', 'SELL', 'HOLD'].includes(latest.action)) { + if (typeof displaySignalOnChart === 'function') { + displaySignalOnChart(latest); + } + } + } + }) + .catch(error => { + console.debug('Could not fetch predictions after refresh:', error); + }); + } catch (error) { + console.debug('Error fetching predictions:', error); + } + } + /** * Add annotation to charts */ @@ -2643,6 +2738,13 @@ class ChartManager { // Add Transformer predictions (star markers with trend lines + ghost candles) if (predictions.transformer) { + console.log(`[updatePredictions] Processing transformer prediction:`, { + action: predictions.transformer.action, + confidence: predictions.transformer.confidence, + has_predicted_candle: !!predictions.transformer.predicted_candle, + predicted_candle_keys: predictions.transformer.predicted_candle ? Object.keys(predictions.transformer.predicted_candle) : [] + }); + this._addTransformerPrediction(predictions.transformer, predictionShapes, predictionAnnotations); // Add trend vector visualization (shorter projection to avoid zoom issues) @@ -2650,7 +2752,7 @@ class ChartManager { this._addTrendPrediction(predictions.transformer.trend_vector, predictionShapes, predictionAnnotations); } - // Handle Predicted Candles + // Handle Predicted Candles (ghost candles) if (predictions.transformer.predicted_candle) { console.log(`[updatePredictions] predicted_candle data:`, predictions.transformer.predicted_candle); const candleData = predictions.transformer.predicted_candle[timeframe]; @@ -2665,7 +2767,11 @@ class ChartManager { let targetTimestamp; // Get the last real candle timestamp to ensure we predict the NEXT one - const lastRealCandle = chart.data.timestamps[chart.data.timestamps.length - 1]; + // CRITICAL FIX: Use Plotly data structure (chartData[0].x for timestamps) + const candlestickTrace = chartData[0]; // First trace is candlestick + const lastRealCandle = candlestickTrace && candlestickTrace.x && candlestickTrace.x.length > 0 + ? candlestickTrace.x[candlestickTrace.x.length - 1] + : null; if (lastRealCandle) { const lastCandleTime = new Date(lastRealCandle); // Predict for the next candle period @@ -2750,21 +2856,40 @@ class ChartManager { // If we have a stored prediction that matches the CURRENT candle time, show it if (this.lastPredictions && this.lastPredictions[timeframe]) { const lastPred = this.lastPredictions[timeframe]; - const currentTimestamp = chart.data.timestamps[chart.data.timestamps.length - 1]; + // CRITICAL FIX: Use Plotly data structure for timestamps + const candlestickTrace = chartData[0]; + const currentTimestamp = candlestickTrace && candlestickTrace.x && candlestickTrace.x.length > 0 + ? candlestickTrace.x[candlestickTrace.x.length - 1] + : null; - // Compare timestamps (allow small diff for jitter) - if (Math.abs(new Date(lastPred.timestamp).getTime() - new Date(currentTimestamp).getTime()) < 1000) { - this._addShadowCandlePrediction(lastPred.candle, currentTimestamp, predictionTraces); + if (currentTimestamp) { + // Compare timestamps (allow small diff for jitter) + if (Math.abs(new Date(lastPred.timestamp).getTime() - new Date(currentTimestamp).getTime()) < 1000) { + this._addShadowCandlePrediction(lastPred.candle, currentTimestamp, predictionTraces); + } } } } // Update chart layout with predictions if (predictionShapes.length > 0 || predictionAnnotations.length > 0) { + // CRITICAL FIX: Get layout from Plotly element, not chart object + const currentLayout = plotElement._fullLayout || {}; + const existingShapes = currentLayout.shapes || []; + const existingAnnotations = currentLayout.annotations || []; + + // Merge new predictions with existing ones (avoid duplicates) + const allShapes = [...existingShapes, ...predictionShapes]; + const allAnnotations = [...existingAnnotations, ...predictionAnnotations]; + Plotly.relayout(plotId, { - shapes: [...(chart.layout.shapes || []), ...predictionShapes], - annotations: [...(chart.layout.annotations || []), ...predictionAnnotations] + shapes: allShapes, + annotations: allAnnotations }); + + console.log(`[updatePredictions] Added ${predictionShapes.length} shapes and ${predictionAnnotations.length} annotations to ${timeframe} chart`); + } else { + console.debug(`[updatePredictions] No prediction shapes/annotations to add for ${timeframe}`); } // Add prediction traces (ghost candles) @@ -3105,54 +3230,133 @@ class ChartManager { } _addTransformerPrediction(prediction, shapes, annotations) { - const timestamp = new Date(prediction.timestamp || Date.now()); - const currentPrice = prediction.current_price || 0; - const predictedPrice = prediction.predicted_price || currentPrice; + // CRITICAL FIX: Get actual price from chart instead of using normalized prediction prices + const timeframe = window.appState?.currentTimeframes?.[0] || '1m'; + const chart = this.charts[timeframe]; + if (!chart) { + console.warn(`[Transformer Prediction] Chart not found for timeframe: ${timeframe}`); + return; + } + + // Get actual current price from the last candle on the chart + let actualCurrentPrice = 0; + const plotElement = document.getElementById(chart.plotId); + if (plotElement && plotElement.data && plotElement.data.length > 0) { + const candlestickTrace = plotElement.data[0]; // First trace is candlestick + if (candlestickTrace && candlestickTrace.close && candlestickTrace.close.length > 0) { + actualCurrentPrice = candlestickTrace.close[candlestickTrace.close.length - 1]; + } + } + + // Fallback to prediction price if chart price not available (but check if it's normalized) + if (actualCurrentPrice === 0 || actualCurrentPrice < 1) { + // Price might be normalized, try to use prediction price + actualCurrentPrice = prediction.current_price || 0; + // If still looks normalized (< 1), we can't display it properly + if (actualCurrentPrice < 1) { + console.warn('[Transformer Prediction] Price appears normalized, cannot display on chart. Chart price:', actualCurrentPrice); + return; + } + } + + // CRITICAL FIX: Parse timestamp correctly (handle both ISO strings and Date objects) + let timestamp; + if (prediction.timestamp) { + if (typeof prediction.timestamp === 'string') { + // Handle various timestamp formats + timestamp = new Date(prediction.timestamp); + if (isNaN(timestamp.getTime())) { + // Try parsing as GMT format + timestamp = new Date(prediction.timestamp.replace('GMT', 'UTC')); + } + } else { + timestamp = new Date(prediction.timestamp); + } + } else { + timestamp = new Date(); + } + + // Use current time if timestamp parsing failed + if (isNaN(timestamp.getTime())) { + timestamp = new Date(); + } const confidence = prediction.confidence || 0; const priceChange = prediction.price_change || 0; const horizonMinutes = prediction.horizon_minutes || 10; - if (confidence < 0.3 || currentPrice === 0) return; + if (confidence < 0.3 || actualCurrentPrice === 0) return; + + // CRITICAL: Calculate predicted price from actual current price and price change + // priceChange is typically a percentage or ratio + let actualPredictedPrice; + if (prediction.predicted_price && prediction.predicted_price > 1) { + // Use predicted_price if it looks like actual price (not normalized) + actualPredictedPrice = prediction.predicted_price; + } else if (typeof priceChange === 'number') { + // Calculate from price change (could be percentage or ratio) + if (Math.abs(priceChange) > 10) { + // Looks like percentage (e.g., 1.0 = 1%) + actualPredictedPrice = actualCurrentPrice * (1 + priceChange / 100); + } else { + // Looks like ratio (e.g., 0.01 = 1%) + actualPredictedPrice = actualCurrentPrice * (1 + priceChange); + } + } else { + // Fallback: use action to determine direction + if (prediction.action === 'BUY') { + actualPredictedPrice = actualCurrentPrice * 1.01; // +1% + } else if (prediction.action === 'SELL') { + actualPredictedPrice = actualCurrentPrice * 0.99; // -1% + } else { + actualPredictedPrice = actualCurrentPrice; // HOLD + } + } // Calculate end time const endTime = new Date(timestamp.getTime() + horizonMinutes * 60 * 1000); - // Determine color based on price change + // Determine color based on action or price change let color; - if (priceChange > 0.5) { - color = 'rgba(0, 200, 255, 0.6)'; // Cyan for UP - } else if (priceChange < -0.5) { - color = 'rgba(255, 100, 0, 0.6)'; // Orange for DOWN + if (prediction.action === 'BUY' || (priceChange > 0 && priceChange > 0.5)) { + color = 'rgba(0, 200, 255, 0.6)'; // Cyan for UP/BUY + } else if (prediction.action === 'SELL' || (priceChange < 0 && priceChange < -0.5)) { + color = 'rgba(255, 100, 0, 0.6)'; // Orange for DOWN/SELL } else { - color = 'rgba(150, 150, 255, 0.5)'; // Light blue for STABLE + color = 'rgba(150, 150, 255, 0.5)'; // Light blue for STABLE/HOLD } - // Add trend line + // Add trend line from current price to predicted price shapes.push({ type: 'line', x0: timestamp, - y0: currentPrice, + y0: actualCurrentPrice, x1: endTime, - y1: predictedPrice, + y1: actualPredictedPrice, line: { color: color, width: 2 + confidence * 2, dash: 'dashdot' - } + }, + layer: 'above' }); - // Add star marker at target + // Add star marker at target with action label + const actionText = prediction.action === 'BUY' ? '▲' : prediction.action === 'SELL' ? '▼' : '★'; annotations.push({ x: endTime, - y: predictedPrice, - text: '★', + y: actualPredictedPrice, + text: `${actionText} ${(confidence * 100).toFixed(0)}%`, showarrow: false, font: { - size: 14 + confidence * 6, + size: 12 + confidence * 4, color: color }, - opacity: 0.6 + confidence * 0.4 + bgcolor: 'rgba(31, 41, 55, 0.8)', + borderpad: 3, + opacity: 0.8 + confidence * 0.2 }); + + console.log(`[Transformer Prediction] Added prediction marker: ${prediction.action} @ ${actualCurrentPrice.toFixed(2)} -> ${actualPredictedPrice.toFixed(2)} (${(confidence * 100).toFixed(1)}% confidence)`); } /** diff --git a/ANNOTATE/web/static/js/live_updates_polling.js b/ANNOTATE/web/static/js/live_updates_polling.js index fccc1de..ddd2b32 100644 --- a/ANNOTATE/web/static/js/live_updates_polling.js +++ b/ANNOTATE/web/static/js/live_updates_polling.js @@ -70,15 +70,31 @@ class LiveUpdatesPolling { .then(response => response.json()) .then(data => { if (data.success) { - // Handle chart update + // Handle chart update (even if null, predictions should still be processed) if (data.chart_update && this.onChartUpdate) { this.onChartUpdate(data.chart_update); } - // Handle prediction update + // CRITICAL FIX: Handle prediction update properly + // data.prediction is already in format { transformer: {...}, dqn: {...}, cnn: {...} } if (data.prediction && this.onPredictionUpdate) { + // Log prediction data for debugging + 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 + }); + + // Pass the prediction object directly (it's already in the correct format) this.onPredictionUpdate(data.prediction); + } else if (!data.prediction) { + console.debug('[Live Updates] No prediction data in response'); } + } else { + console.debug('[Live Updates] Response not successful:', data); } }) .catch(error => { @@ -148,24 +164,51 @@ document.addEventListener('DOMContentLoaded', function() { }; 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 + // Update prediction display in UI if (typeof updatePredictionDisplay === 'function') { - updatePredictionDisplay(data); + // 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 + // Add to prediction history (use transformer prediction if available) if (typeof predictionHistory !== 'undefined') { - predictionHistory.unshift(data); - if (predictionHistory.length > 5) { - predictionHistory = predictionHistory.slice(0, 5); - } - if (typeof updatePredictionHistory === 'function') { - updatePredictionHistory(); + 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(); + } } } };