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:
# 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

View File

@@ -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;
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)`);
}
/**

View File

@@ -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,19 +164,45 @@ 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);
const predictionToAdd = data.transformer || data.dqn || data.cnn || data;
if (predictionToAdd) {
predictionHistory.unshift(predictionToAdd);
if (predictionHistory.length > 5) {
predictionHistory = predictionHistory.slice(0, 5);
}
@@ -168,6 +210,7 @@ document.addEventListener('DOMContentLoaded', function() {
updatePredictionHistory();
}
}
}
};
// Function to subscribe to all active timeframes