wip
This commit is contained in:
@@ -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)`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user