predictions display

This commit is contained in:
Dobromir Popov
2025-12-08 22:59:16 +02:00
parent 5a4b0cf35b
commit 8c3dc5423e
3 changed files with 301 additions and 44 deletions

View File

@@ -2525,8 +2525,18 @@ class AnnotationDashboard:
if transformer_preds: if transformer_preds:
# Convert any remaining tensors to Python types before JSON serialization # Convert any remaining tensors to Python types before JSON serialization
transformer_pred = transformer_preds[-1].copy() 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) 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: if predictions:
response['prediction'] = predictions response['prediction'] = predictions

View File

@@ -1021,7 +1021,8 @@ class ChartManager {
return close >= data.open[i] ? '#10b981' : '#ef4444'; 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 = [ const chartData = [
{ {
x: data.timestamps, x: data.timestamps,
@@ -1031,6 +1032,7 @@ class ChartManager {
close: data.close, close: data.close,
type: 'candlestick', type: 'candlestick',
name: 'Price', name: 'Price',
yaxis: 'y', // Explicitly set to price axis (top)
increasing: { increasing: {
line: { color: '#10b981', width: 1 }, line: { color: '#10b981', width: 1 },
fillcolor: '#10b981' fillcolor: '#10b981'
@@ -1044,7 +1046,7 @@ class ChartManager {
x: data.timestamps, x: data.timestamps,
y: data.volume, y: data.volume,
type: 'bar', type: 'bar',
yaxis: 'y2', yaxis: 'y2', // Explicitly set to volume axis (bottom)
name: 'Volume', name: 'Volume',
marker: { marker: {
color: volumeColors, color: volumeColors,
@@ -1183,16 +1185,109 @@ class ChartManager {
} }
} }
// Use Plotly.react for efficient updates // CRITICAL FIX: Preserve existing layout (theme, yaxis domains, etc.) when updating
const update = { 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, 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 * Add annotation to charts
*/ */
@@ -2643,6 +2738,13 @@ class ChartManager {
// Add Transformer predictions (star markers with trend lines + ghost candles) // Add Transformer predictions (star markers with trend lines + ghost candles)
if (predictions.transformer) { 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); this._addTransformerPrediction(predictions.transformer, predictionShapes, predictionAnnotations);
// Add trend vector visualization (shorter projection to avoid zoom issues) // Add trend vector visualization (shorter projection to avoid zoom issues)
@@ -2650,7 +2752,7 @@ class ChartManager {
this._addTrendPrediction(predictions.transformer.trend_vector, predictionShapes, predictionAnnotations); this._addTrendPrediction(predictions.transformer.trend_vector, predictionShapes, predictionAnnotations);
} }
// Handle Predicted Candles // Handle Predicted Candles (ghost candles)
if (predictions.transformer.predicted_candle) { if (predictions.transformer.predicted_candle) {
console.log(`[updatePredictions] predicted_candle data:`, predictions.transformer.predicted_candle); console.log(`[updatePredictions] predicted_candle data:`, predictions.transformer.predicted_candle);
const candleData = predictions.transformer.predicted_candle[timeframe]; const candleData = predictions.transformer.predicted_candle[timeframe];
@@ -2665,7 +2767,11 @@ class ChartManager {
let targetTimestamp; let targetTimestamp;
// Get the last real candle timestamp to ensure we predict the NEXT one // 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) { if (lastRealCandle) {
const lastCandleTime = new Date(lastRealCandle); const lastCandleTime = new Date(lastRealCandle);
// Predict for the next candle period // 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 we have a stored prediction that matches the CURRENT candle time, show it
if (this.lastPredictions && this.lastPredictions[timeframe]) { if (this.lastPredictions && this.lastPredictions[timeframe]) {
const lastPred = 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 (currentTimestamp) {
if (Math.abs(new Date(lastPred.timestamp).getTime() - new Date(currentTimestamp).getTime()) < 1000) { // Compare timestamps (allow small diff for jitter)
this._addShadowCandlePrediction(lastPred.candle, currentTimestamp, predictionTraces); if (Math.abs(new Date(lastPred.timestamp).getTime() - new Date(currentTimestamp).getTime()) < 1000) {
this._addShadowCandlePrediction(lastPred.candle, currentTimestamp, predictionTraces);
}
} }
} }
} }
// Update chart layout with predictions // Update chart layout with predictions
if (predictionShapes.length > 0 || predictionAnnotations.length > 0) { 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, { Plotly.relayout(plotId, {
shapes: [...(chart.layout.shapes || []), ...predictionShapes], shapes: allShapes,
annotations: [...(chart.layout.annotations || []), ...predictionAnnotations] 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) // Add prediction traces (ghost candles)
@@ -3105,54 +3230,133 @@ class ChartManager {
} }
_addTransformerPrediction(prediction, shapes, annotations) { _addTransformerPrediction(prediction, shapes, annotations) {
const timestamp = new Date(prediction.timestamp || Date.now()); // CRITICAL FIX: Get actual price from chart instead of using normalized prediction prices
const currentPrice = prediction.current_price || 0; const timeframe = window.appState?.currentTimeframes?.[0] || '1m';
const predictedPrice = prediction.predicted_price || currentPrice; 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 confidence = prediction.confidence || 0;
const priceChange = prediction.price_change || 0; const priceChange = prediction.price_change || 0;
const horizonMinutes = prediction.horizon_minutes || 10; 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 // Calculate end time
const endTime = new Date(timestamp.getTime() + horizonMinutes * 60 * 1000); 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; let color;
if (priceChange > 0.5) { if (prediction.action === 'BUY' || (priceChange > 0 && priceChange > 0.5)) {
color = 'rgba(0, 200, 255, 0.6)'; // Cyan for UP color = 'rgba(0, 200, 255, 0.6)'; // Cyan for UP/BUY
} else if (priceChange < -0.5) { } else if (prediction.action === 'SELL' || (priceChange < 0 && priceChange < -0.5)) {
color = 'rgba(255, 100, 0, 0.6)'; // Orange for DOWN color = 'rgba(255, 100, 0, 0.6)'; // Orange for DOWN/SELL
} else { } 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({ shapes.push({
type: 'line', type: 'line',
x0: timestamp, x0: timestamp,
y0: currentPrice, y0: actualCurrentPrice,
x1: endTime, x1: endTime,
y1: predictedPrice, y1: actualPredictedPrice,
line: { line: {
color: color, color: color,
width: 2 + confidence * 2, width: 2 + confidence * 2,
dash: 'dashdot' 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({ annotations.push({
x: endTime, x: endTime,
y: predictedPrice, y: actualPredictedPrice,
text: '★', text: `${actionText} ${(confidence * 100).toFixed(0)}%`,
showarrow: false, showarrow: false,
font: { font: {
size: 14 + confidence * 6, size: 12 + confidence * 4,
color: color 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)`);
} }
/** /**

View File

@@ -70,15 +70,31 @@ class LiveUpdatesPolling {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
// Handle chart update // Handle chart update (even if null, predictions should still be processed)
if (data.chart_update && this.onChartUpdate) { if (data.chart_update && this.onChartUpdate) {
this.onChartUpdate(data.chart_update); 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) { 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); 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 => { .catch(error => {
@@ -148,24 +164,51 @@ document.addEventListener('DOMContentLoaded', function() {
}; };
window.liveUpdatesPolling.onPredictionUpdate = function(data) { 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 // Update prediction visualization on charts
if (window.appState && window.appState.chartManager) { 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); window.appState.chartManager.updatePredictions(data);
} }
// Update prediction display // Update prediction display in UI
if (typeof updatePredictionDisplay === 'function') { 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') { if (typeof predictionHistory !== 'undefined') {
predictionHistory.unshift(data); const predictionToAdd = data.transformer || data.dqn || data.cnn || data;
if (predictionHistory.length > 5) { if (predictionToAdd) {
predictionHistory = predictionHistory.slice(0, 5); predictionHistory.unshift(predictionToAdd);
} if (predictionHistory.length > 5) {
if (typeof updatePredictionHistory === 'function') { predictionHistory = predictionHistory.slice(0, 5);
updatePredictionHistory(); }
if (typeof updatePredictionHistory === 'function') {
updatePredictionHistory();
}
} }
} }
}; };