This commit is contained in:
Dobromir Popov
2025-12-09 00:34:51 +02:00
parent 8c3dc5423e
commit d6ada4b416
5 changed files with 534 additions and 429 deletions

View File

@@ -18,6 +18,8 @@ class ChartManager {
this.ghostCandleHistory = {}; // Store ghost candles per timeframe (max 50 each)
this.maxGhostCandles = 150; // Maximum number of ghost candles to keep
this.modelAccuracyMetrics = {}; // Track overall model accuracy per timeframe
this.predictionHistory = []; // Store last 20 predictions with fading
this.maxPredictions = 20; // Maximum number of predictions to display
// Helper to ensure all timestamps are in UTC
this.normalizeTimestamp = (timestamp) => {
@@ -310,15 +312,34 @@ class ChartManager {
};
}
// Parse timestamp - format to match chart data format
const candleTimestamp = new Date(candle.timestamp);
// CRITICAL FIX: Parse timestamp ensuring UTC handling
// Backend now sends ISO format with 'Z' (e.g., '2025-12-08T21:00:00Z')
// JavaScript Date will parse this correctly as UTC
let candleTimestamp;
if (typeof candle.timestamp === 'string') {
// If it's already ISO format with 'Z', parse directly
if (candle.timestamp.includes('T') && (candle.timestamp.endsWith('Z') || candle.timestamp.includes('+'))) {
candleTimestamp = new Date(candle.timestamp);
} else if (candle.timestamp.includes('T')) {
// ISO format without timezone - assume UTC
candleTimestamp = new Date(candle.timestamp + 'Z');
} else {
// Old format: 'YYYY-MM-DD HH:MM:SS' - convert to ISO and treat as UTC
candleTimestamp = new Date(candle.timestamp.replace(' ', 'T') + 'Z');
}
} else {
candleTimestamp = new Date(candle.timestamp);
}
// Format using UTC methods and ISO format with 'Z' for consistency
const year = candleTimestamp.getUTCFullYear();
const month = String(candleTimestamp.getUTCMonth() + 1).padStart(2, '0');
const day = String(candleTimestamp.getUTCDate()).padStart(2, '0');
const hours = String(candleTimestamp.getUTCHours()).padStart(2, '0');
const minutes = String(candleTimestamp.getUTCMinutes()).padStart(2, '0');
const seconds = String(candleTimestamp.getUTCSeconds()).padStart(2, '0');
const formattedTimestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
// Format as ISO with 'Z' so it's consistently treated as UTC
const formattedTimestamp = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`;
// Get current chart data from Plotly
const chartData = Plotly.Plots.data(plotId);
@@ -364,7 +385,13 @@ class ChartManager {
chart.data.close.push(candle.close);
chart.data.volume.push(candle.volume);
console.log(`[${timeframe}] Added new candle: ${formattedTimestamp}`);
console.log(`[${timeframe}] Added new candle: ${formattedTimestamp}`, {
open: candle.open,
high: candle.high,
low: candle.low,
close: candle.close,
volume: candle.volume
});
} else {
// Update last candle - update both Plotly and internal data structure
const x = [...candlestickTrace.x];
@@ -632,27 +659,31 @@ class ChartManager {
const ghostTime = new Date(furthestGhost.targetTime);
const currentMax = new Date(xMax);
if (ghostTime > currentMax) {
const year = ghostTime.getUTCFullYear();
const month = String(ghostTime.getUTCMonth() + 1).padStart(2, '0');
const day = String(ghostTime.getUTCDate()).padStart(2, '0');
const hours = String(ghostTime.getUTCHours()).padStart(2, '0');
const minutes = String(ghostTime.getUTCMinutes()).padStart(2, '0');
const seconds = String(ghostTime.getUTCSeconds()).padStart(2, '0');
xMax = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
// CRITICAL FIX: Format as ISO with 'Z' to match chart timestamp format
xMax = ghostTime.toISOString();
console.log(`[${timeframe}] Pivot lines extended to include ${ghosts.length} ghost candles (to ${xMax})`);
}
}
}
// Process each timestamp that has pivot markers
Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => {
// CRITICAL FIX: Ensure pivot marker timestamps are in ISO format
Object.entries(data.pivot_markers).forEach(([timestampKey, pivots]) => {
// Convert pivot marker timestamp to ISO format if needed
let pivotTimestamp = timestampKey;
if (typeof timestampKey === 'string' && !timestampKey.includes('T')) {
pivotTimestamp = new Date(timestampKey.replace(' ', 'T') + 'Z').toISOString();
} else if (typeof timestampKey === 'string' && !timestampKey.endsWith('Z') && !timestampKey.includes('+')) {
pivotTimestamp = new Date(timestampKey + 'Z').toISOString();
}
// Process high pivots
if (pivots.highs && pivots.highs.length > 0) {
pivots.highs.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'high');
// Draw dot on the pivot candle (above the high)
pivotDots.x.push(timestamp);
// Draw dot on the pivot candle (above the high) - use converted timestamp
pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color);
@@ -698,8 +729,8 @@ class ChartManager {
pivots.lows.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'low');
// Draw dot on the pivot candle (below the low)
pivotDots.x.push(timestamp);
// Draw dot on the pivot candle (below the low) - use converted timestamp
pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color);
@@ -1061,38 +1092,57 @@ class ChartManager {
const annotations = [];
const pivotDots = { x: [], y: [], text: [], marker: { color: [], size: [], symbol: [] }, mode: 'markers', hoverinfo: 'text', showlegend: false };
if (data.pivot_markers && Object.keys(data.pivot_markers).length > 0) {
const xMin = data.timestamps[0];
let xMax = data.timestamps[data.timestamps.length - 1];
// Extend xMax to include ghost candle predictions if they exist
if (this.ghostCandleHistory && this.ghostCandleHistory[timeframe] && this.ghostCandleHistory[timeframe].length > 0) {
const ghosts = this.ghostCandleHistory[timeframe];
const furthestGhost = ghosts[ghosts.length - 1];
if (furthestGhost && furthestGhost.targetTime) {
const ghostTime = new Date(furthestGhost.targetTime);
const currentMax = new Date(xMax);
if (ghostTime > currentMax) {
const year = ghostTime.getUTCFullYear();
const month = String(ghostTime.getUTCMonth() + 1).padStart(2, '0');
const day = String(ghostTime.getUTCDate()).padStart(2, '0');
const hours = String(ghostTime.getUTCHours()).padStart(2, '0');
const minutes = String(ghostTime.getUTCMinutes()).padStart(2, '0');
const seconds = String(ghostTime.getUTCSeconds()).padStart(2, '0');
xMax = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
}
if (data.pivot_markers && Object.keys(data.pivot_markers).length > 0) {
// CRITICAL FIX: Ensure timestamps are in ISO format for consistency
// Parse timestamps to ensure they're treated as UTC
let xMin = data.timestamps[0];
let xMax = data.timestamps[data.timestamps.length - 1];
// Convert to ISO format if not already
if (typeof xMin === 'string' && !xMin.includes('T')) {
xMin = new Date(xMin.replace(' ', 'T') + 'Z').toISOString();
} else if (typeof xMin === 'string' && !xMin.endsWith('Z') && !xMin.includes('+')) {
xMin = new Date(xMin + 'Z').toISOString();
}
if (typeof xMax === 'string' && !xMax.includes('T')) {
xMax = new Date(xMax.replace(' ', 'T') + 'Z').toISOString();
} else if (typeof xMax === 'string' && !xMax.endsWith('Z') && !xMax.includes('+')) {
xMax = new Date(xMax + 'Z').toISOString();
}
// Extend xMax to include ghost candle predictions if they exist
if (this.ghostCandleHistory && this.ghostCandleHistory[timeframe] && this.ghostCandleHistory[timeframe].length > 0) {
const ghosts = this.ghostCandleHistory[timeframe];
const furthestGhost = ghosts[ghosts.length - 1];
if (furthestGhost && furthestGhost.targetTime) {
const ghostTime = new Date(furthestGhost.targetTime);
const currentMax = new Date(xMax);
if (ghostTime > currentMax) {
// Format as ISO with 'Z' to match chart timestamp format
xMax = ghostTime.toISOString();
}
}
}
// Process each timestamp that has pivot markers
Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => {
// CRITICAL FIX: Ensure pivot marker timestamps are in ISO format
Object.entries(data.pivot_markers).forEach(([timestampKey, pivots]) => {
// Convert pivot marker timestamp to ISO format if needed
let pivotTimestamp = timestampKey;
if (typeof timestampKey === 'string' && !timestampKey.includes('T')) {
pivotTimestamp = new Date(timestampKey.replace(' ', 'T') + 'Z').toISOString();
} else if (typeof timestampKey === 'string' && !timestampKey.endsWith('Z') && !timestampKey.includes('+')) {
pivotTimestamp = new Date(timestampKey + 'Z').toISOString();
}
// Process high pivots
if (pivots.highs && pivots.highs.length > 0) {
pivots.highs.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'high');
// Draw dot on the pivot candle
pivotDots.x.push(timestamp);
// Draw dot on the pivot candle - use converted timestamp
pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color);
@@ -1137,8 +1187,8 @@ class ChartManager {
pivots.lows.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'low');
// Draw dot on the pivot candle
pivotDots.x.push(timestamp);
// Draw dot on the pivot candle - use converted timestamp
pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: $${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color);
@@ -2121,26 +2171,31 @@ class ChartManager {
const ghostTime = new Date(furthestGhost.targetTime);
const currentMax = new Date(xMax);
if (ghostTime > currentMax) {
const year = ghostTime.getUTCFullYear();
const month = String(ghostTime.getUTCMonth() + 1).padStart(2, '0');
const day = String(ghostTime.getUTCDate()).padStart(2, '0');
const hours = String(ghostTime.getUTCHours()).padStart(2, '0');
const minutes = String(ghostTime.getUTCMinutes()).padStart(2, '0');
const seconds = String(ghostTime.getUTCSeconds()).padStart(2, '0');
xMax = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
// CRITICAL FIX: Format as ISO with 'Z' to match chart timestamp format
xMax = ghostTime.toISOString();
console.log(`[${timeframe}] Pivot lines extended to include ${ghosts.length} ghost candles (to ${xMax})`);
}
}
}
// Process each timestamp that has pivot markers
Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => {
// CRITICAL FIX: Ensure pivot marker timestamps are in ISO format
Object.entries(data.pivot_markers).forEach(([timestampKey, pivots]) => {
// Convert pivot marker timestamp to ISO format if needed
let pivotTimestamp = timestampKey;
if (typeof timestampKey === 'string' && !timestampKey.includes('T')) {
pivotTimestamp = new Date(timestampKey.replace(' ', 'T') + 'Z').toISOString();
} else if (typeof timestampKey === 'string' && !timestampKey.endsWith('Z') && !timestampKey.includes('+')) {
pivotTimestamp = new Date(timestampKey + 'Z').toISOString();
}
// Process high pivots
if (pivots.highs && pivots.highs.length > 0) {
pivots.highs.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'high');
pivotDots.x.push(timestamp);
// CRITICAL FIX: Use converted timestamp for consistency
pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} High Pivot<br>Price: ${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color);
@@ -2174,7 +2229,8 @@ class ChartManager {
pivots.lows.forEach(pivot => {
const color = this._getPivotColor(pivot.level, 'low');
pivotDots.x.push(timestamp);
// CRITICAL FIX: Use converted timestamp for consistency
pivotDots.x.push(pivotTimestamp);
pivotDots.y.push(pivot.price);
pivotDots.text.push(`L${pivot.level} Low Pivot<br>Price: ${pivot.price.toFixed(2)}<br>Strength: ${(pivot.strength * 100).toFixed(0)}%`);
pivotDots.marker.color.push(color);
@@ -2736,137 +2792,170 @@ class ChartManager {
this._addCNNPrediction(predictions.cnn, predictionShapes, predictionAnnotations);
}
// Add Transformer predictions (star markers with trend lines + ghost candles)
// CRITICAL FIX: Manage prediction history (max 20, fade oldest)
// Add new transformer prediction to history
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) : []
});
// Check if this is a new prediction (different timestamp or significant change)
const newPred = predictions.transformer;
const isNew = !this.predictionHistory.length ||
this.predictionHistory[0].timestamp !== newPred.timestamp ||
Math.abs((this.predictionHistory[0].confidence || 0) - (newPred.confidence || 0)) > 0.01;
this._addTransformerPrediction(predictions.transformer, predictionShapes, predictionAnnotations);
// Add trend vector visualization (shorter projection to avoid zoom issues)
if (predictions.transformer.trend_vector) {
this._addTrendPrediction(predictions.transformer.trend_vector, predictionShapes, predictionAnnotations);
}
// 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];
console.log(`[updatePredictions] candleData for ${timeframe}:`, candleData);
if (candleData) {
// Get the prediction timestamp from the model (when inference was made)
const predictionTimestamp = predictions.transformer.timestamp || new Date().toISOString();
// Calculate the target timestamp (when this prediction is for)
// This should be the NEXT candle after the inference time
const inferenceTime = new Date(predictionTimestamp);
let targetTimestamp;
// Get the last real candle timestamp to ensure we predict the NEXT one
// 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
if (timeframe === '1s') {
targetTimestamp = new Date(lastCandleTime.getTime() + 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(lastCandleTime.getTime() + 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(lastCandleTime.getTime() + 3600000);
} else {
targetTimestamp = new Date(lastCandleTime.getTime() + 60000);
}
} else {
// Fallback to inference time + period if no real candles yet
if (timeframe === '1s') {
targetTimestamp = new Date(inferenceTime.getTime() + 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(inferenceTime.getTime() + 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(inferenceTime.getTime() + 3600000);
} else {
targetTimestamp = new Date(inferenceTime.getTime() + 60000);
}
}
// Round to exact candle boundary to prevent bunching
if (timeframe === '1s') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 1000) * 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 60000) * 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 3600000) * 3600000);
}
// 1. Initialize ghost candle history for this timeframe if needed
if (!this.ghostCandleHistory[timeframe]) {
this.ghostCandleHistory[timeframe] = [];
}
// 2. Add new ghost candle to history
const year = targetTimestamp.getUTCFullYear();
const month = String(targetTimestamp.getUTCMonth() + 1).padStart(2, '0');
const day = String(targetTimestamp.getUTCDate()).padStart(2, '0');
const hours = String(targetTimestamp.getUTCHours()).padStart(2, '0');
const minutes = String(targetTimestamp.getUTCMinutes()).padStart(2, '0');
const seconds = String(targetTimestamp.getUTCSeconds()).padStart(2, '0');
const formattedTimestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
this.ghostCandleHistory[timeframe].push({
timestamp: formattedTimestamp,
candle: candleData,
targetTime: targetTimestamp
});
// 3. Keep only last 10 ghost candles
if (this.ghostCandleHistory[timeframe].length > this.maxGhostCandles) {
this.ghostCandleHistory[timeframe] = this.ghostCandleHistory[timeframe].slice(-this.maxGhostCandles);
}
// 4. Add all ghost candles from history to traces (with accuracy if validated)
for (const ghost of this.ghostCandleHistory[timeframe]) {
this._addGhostCandlePrediction(ghost.candle, timeframe, predictionTraces, ghost.targetTime, ghost.accuracy);
}
// 5. Store as "Last Prediction" for shadow rendering
if (!this.lastPredictions) this.lastPredictions = {};
this.lastPredictions[timeframe] = {
timestamp: targetTimestamp.toISOString(),
candle: candleData,
inferenceTime: predictionTimestamp
};
console.log(`[${timeframe}] Ghost candle added (${this.ghostCandleHistory[timeframe].length}/${this.maxGhostCandles}) at ${targetTimestamp.toISOString()}`, {
predicted: candleData,
timestamp: formattedTimestamp
});
if (isNew) {
// Add to history (most recent first)
this.predictionHistory.unshift({
...newPred,
addedAt: Date.now()
});
// Keep only last 20 predictions
if (this.predictionHistory.length > this.maxPredictions) {
this.predictionHistory = this.predictionHistory.slice(0, this.maxPredictions);
}
}
// 3. Render "Shadow Candle" (Previous Prediction for Current Candle)
// 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];
// 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;
console.log(`[updatePredictions] Processing ${this.predictionHistory.length} predictions (new: ${isNew}):`, {
action: newPred.action,
confidence: newPred.confidence,
has_predicted_candle: !!newPred.predicted_candle
});
}
// Render all predictions from history with fading (oldest = most transparent)
this.predictionHistory.forEach((pred, index) => {
// Calculate opacity: newest = 1.0, oldest = 0.2
const ageRatio = index / Math.max(1, this.predictionHistory.length - 1);
const baseOpacity = 1.0 - (ageRatio * 0.8); // Fade from 1.0 to 0.2
// Create a copy of prediction with opacity applied
const fadedPred = {
...pred,
_fadeOpacity: baseOpacity
};
this._addTransformerPrediction(fadedPred, predictionShapes, predictionAnnotations);
// Add trend vector visualization (shorter projection to avoid zoom issues)
if (pred.trend_vector) {
this._addTrendPrediction(pred.trend_vector, predictionShapes, predictionAnnotations);
}
});
// Handle Predicted Candles (ghost candles) - only for the most recent prediction
if (predictions.transformer && predictions.transformer.predicted_candle) {
console.log(`[updatePredictions] predicted_candle data:`, predictions.transformer.predicted_candle);
const candleData = predictions.transformer.predicted_candle[timeframe];
console.log(`[updatePredictions] candleData for ${timeframe}:`, candleData);
if (candleData) {
// Get the prediction timestamp from the model (when inference was made)
const predictionTimestamp = predictions.transformer.timestamp || new Date().toISOString();
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);
// Calculate the target timestamp (when this prediction is for)
// This should be the NEXT candle after the inference time
const inferenceTime = new Date(predictionTimestamp);
let targetTimestamp;
// Get the last real candle timestamp to ensure we predict the NEXT one
// 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
if (timeframe === '1s') {
targetTimestamp = new Date(lastCandleTime.getTime() + 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(lastCandleTime.getTime() + 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(lastCandleTime.getTime() + 3600000);
} else {
targetTimestamp = new Date(lastCandleTime.getTime() + 60000);
}
} else {
// Fallback to inference time + period if no real candles yet
if (timeframe === '1s') {
targetTimestamp = new Date(inferenceTime.getTime() + 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(inferenceTime.getTime() + 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(inferenceTime.getTime() + 3600000);
} else {
targetTimestamp = new Date(inferenceTime.getTime() + 60000);
}
}
// Round to exact candle boundary to prevent bunching
if (timeframe === '1s') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 1000) * 1000);
} else if (timeframe === '1m') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 60000) * 60000);
} else if (timeframe === '1h') {
targetTimestamp = new Date(Math.floor(targetTimestamp.getTime() / 3600000) * 3600000);
}
// 1. Initialize ghost candle history for this timeframe if needed
if (!this.ghostCandleHistory[timeframe]) {
this.ghostCandleHistory[timeframe] = [];
}
// 2. Add new ghost candle to history
const year = targetTimestamp.getUTCFullYear();
const month = String(targetTimestamp.getUTCMonth() + 1).padStart(2, '0');
const day = String(targetTimestamp.getUTCDate()).padStart(2, '0');
const hours = String(targetTimestamp.getUTCHours()).padStart(2, '0');
const minutes = String(targetTimestamp.getUTCMinutes()).padStart(2, '0');
const seconds = String(targetTimestamp.getUTCSeconds()).padStart(2, '0');
const formattedTimestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
this.ghostCandleHistory[timeframe].push({
timestamp: formattedTimestamp,
candle: candleData,
targetTime: targetTimestamp
});
// 3. Keep only last 10 ghost candles
if (this.ghostCandleHistory[timeframe].length > this.maxGhostCandles) {
this.ghostCandleHistory[timeframe] = this.ghostCandleHistory[timeframe].slice(-this.maxGhostCandles);
}
// 4. Add all ghost candles from history to traces (with accuracy if validated)
for (const ghost of this.ghostCandleHistory[timeframe]) {
this._addGhostCandlePrediction(ghost.candle, timeframe, predictionTraces, ghost.targetTime, ghost.accuracy);
}
// 5. Store as "Last Prediction" for shadow rendering
if (!this.lastPredictions) this.lastPredictions = {};
this.lastPredictions[timeframe] = {
timestamp: targetTimestamp.toISOString(),
candle: candleData,
inferenceTime: predictionTimestamp
};
console.log(`[${timeframe}] Ghost candle added (${this.ghostCandleHistory[timeframe].length}/${this.maxGhostCandles}) at ${targetTimestamp.toISOString()}`, {
predicted: candleData,
timestamp: formattedTimestamp
});
}
}
// Render "Shadow Candle" (Previous Prediction for Current Candle)
// 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];
// 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);
}
}
}
@@ -3230,7 +3319,8 @@ class ChartManager {
}
_addTransformerPrediction(prediction, shapes, annotations) {
// CRITICAL FIX: Get actual price from chart instead of using normalized prediction prices
// CRITICAL FIX: Use first timeframe from currentTimeframes (ignore Primary Timeline dropdown)
// Always use the first active timeframe, not the dropdown selection
const timeframe = window.appState?.currentTimeframes?.[0] || '1m';
const chart = this.charts[timeframe];
if (!chart) {
@@ -3238,36 +3328,19 @@ class ChartManager {
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)
// CRITICAL FIX: Use prediction's timestamp and price as starting point
// Parse prediction timestamp
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
if (prediction.timestamp.includes('T') && (prediction.timestamp.endsWith('Z') || prediction.timestamp.includes('+'))) {
timestamp = new Date(prediction.timestamp);
} else if (prediction.timestamp.includes('T')) {
timestamp = new Date(prediction.timestamp + 'Z');
} else if (prediction.timestamp.includes('GMT')) {
timestamp = new Date(prediction.timestamp.replace('GMT', 'UTC'));
} else {
timestamp = new Date(prediction.timestamp.replace(' ', 'T') + 'Z');
}
} else {
timestamp = new Date(prediction.timestamp);
@@ -3276,18 +3349,50 @@ class ChartManager {
timestamp = new Date();
}
// Use current time if timestamp parsing failed
// Ensure timestamp is valid
if (isNaN(timestamp.getTime())) {
timestamp = new Date();
}
// Get prediction price - use current_price from prediction
let predictionPrice = prediction.current_price || 0;
// If price looks normalized (< 1), try to get actual price from chart
if (predictionPrice < 1) {
const plotElement = document.getElementById(chart.plotId);
if (plotElement && plotElement.data && plotElement.data.length > 0) {
const candlestickTrace = plotElement.data[0];
if (candlestickTrace && candlestickTrace.close && candlestickTrace.close.length > 0) {
// Find the candle closest to prediction timestamp
const predTimeMs = timestamp.getTime();
let closestPrice = candlestickTrace.close[candlestickTrace.close.length - 1];
let minDiff = Infinity;
for (let i = 0; i < candlestickTrace.x.length; i++) {
const candleTime = new Date(candlestickTrace.x[i]).getTime();
const diff = Math.abs(candleTime - predTimeMs);
if (diff < minDiff) {
minDiff = diff;
closestPrice = candlestickTrace.close[i];
}
}
predictionPrice = closestPrice;
}
}
}
if (predictionPrice === 0 || predictionPrice < 1) {
console.warn('[Transformer Prediction] Cannot determine prediction price');
return;
}
const confidence = prediction.confidence || 0;
const priceChange = prediction.price_change || 0;
const horizonMinutes = prediction.horizon_minutes || 10;
if (confidence < 0.3 || actualCurrentPrice === 0) return;
if (confidence < 0.3) return;
// CRITICAL: Calculate predicted price from actual current price and price change
// priceChange is typically a percentage or ratio
// Calculate predicted price from prediction price and price change
let actualPredictedPrice;
if (prediction.predicted_price && prediction.predicted_price > 1) {
// Use predicted_price if it looks like actual price (not normalized)
@@ -3296,19 +3401,19 @@ class ChartManager {
// 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);
actualPredictedPrice = predictionPrice * (1 + priceChange / 100);
} else {
// Looks like ratio (e.g., 0.01 = 1%)
actualPredictedPrice = actualCurrentPrice * (1 + priceChange);
actualPredictedPrice = predictionPrice * (1 + priceChange);
}
} else {
// Fallback: use action to determine direction
if (prediction.action === 'BUY') {
actualPredictedPrice = actualCurrentPrice * 1.01; // +1%
actualPredictedPrice = predictionPrice * 1.01; // +1%
} else if (prediction.action === 'SELL') {
actualPredictedPrice = actualCurrentPrice * 0.99; // -1%
actualPredictedPrice = predictionPrice * 0.99; // -1%
} else {
actualPredictedPrice = actualCurrentPrice; // HOLD
actualPredictedPrice = predictionPrice; // HOLD
}
}
@@ -3325,16 +3430,37 @@ class ChartManager {
color = 'rgba(150, 150, 255, 0.5)'; // Light blue for STABLE/HOLD
}
// Add trend line from current price to predicted price
// CRITICAL FIX: Format timestamps as ISO strings to match chart data format
const timestampISO = timestamp.toISOString();
const endTimeISO = endTime.toISOString();
// Apply fade opacity if provided (for prediction history)
const fadeOpacity = prediction._fadeOpacity !== undefined ? prediction._fadeOpacity : 1.0;
// Extract RGB from color and apply fade opacity
let fadedColor = color;
if (typeof color === 'string' && color.startsWith('rgba')) {
// Parse rgba and apply fade
const rgbaMatch = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (rgbaMatch) {
const r = parseInt(rgbaMatch[1]);
const g = parseInt(rgbaMatch[2]);
const b = parseInt(rgbaMatch[3]);
const originalAlpha = rgbaMatch[4] ? parseFloat(rgbaMatch[4]) : 0.6;
fadedColor = `rgba(${r}, ${g}, ${b}, ${originalAlpha * fadeOpacity})`;
}
}
// Add trend line from prediction timestamp/price to predicted price
shapes.push({
type: 'line',
x0: timestamp,
y0: actualCurrentPrice,
x1: endTime,
y1: actualPredictedPrice,
x0: timestampISO, // Start at prediction timestamp
y0: predictionPrice, // Start at prediction price
x1: endTimeISO, // End at predicted time
y1: actualPredictedPrice, // End at predicted price
line: {
color: color,
width: 2 + confidence * 2,
color: fadedColor,
width: (2 + confidence * 2) * fadeOpacity, // Also fade width slightly
dash: 'dashdot'
},
layer: 'above'
@@ -3343,20 +3469,20 @@ class ChartManager {
// Add star marker at target with action label
const actionText = prediction.action === 'BUY' ? '▲' : prediction.action === 'SELL' ? '▼' : '★';
annotations.push({
x: endTime,
x: endTimeISO, // Use ISO string format to match chart timestamps
y: actualPredictedPrice,
text: `${actionText} ${(confidence * 100).toFixed(0)}%`,
showarrow: false,
font: {
size: 12 + confidence * 4,
color: color
size: (12 + confidence * 4) * fadeOpacity, // Fade font size
color: fadedColor
},
bgcolor: 'rgba(31, 41, 55, 0.8)',
bgcolor: `rgba(31, 41, 55, ${0.8 * fadeOpacity})`, // Fade background
borderpad: 3,
opacity: 0.8 + confidence * 0.2
opacity: (0.8 + confidence * 0.2) * fadeOpacity // Apply fade to overall opacity
});
console.log(`[Transformer Prediction] Added prediction marker: ${prediction.action} @ ${actualCurrentPrice.toFixed(2)} -> ${actualPredictedPrice.toFixed(2)} (${(confidence * 100).toFixed(1)}% confidence)`);
console.log(`[Transformer Prediction] Added prediction marker: ${prediction.action} @ ${predictionPrice.toFixed(2)} -> ${actualPredictedPrice.toFixed(2)} (${(confidence * 100).toFixed(1)}% confidence)`);
}
/**