fix 1s 1m chart less candles ;

fix vertical zoom
This commit is contained in:
Dobromir Popov
2025-11-22 18:23:04 +02:00
parent 26cbfd771b
commit 4b93b6fd42
4 changed files with 664 additions and 11 deletions

View File

@@ -17,6 +17,7 @@ class ChartManager {
this.lastPredictionHash = null; // Track if predictions actually changed
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
// Helper to ensure all timestamps are in UTC
this.normalizeTimestamp = (timestamp) => {
@@ -81,7 +82,8 @@ class ChartManager {
*/
async updateChart(timeframe) {
try {
const response = await fetch(`/api/chart-data?timeframe=${timeframe}&limit=1000`);
// Use consistent candle count across all timeframes (2500 for sufficient training context)
const response = await fetch(`/api/chart-data?timeframe=${timeframe}&limit=2500`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
@@ -109,7 +111,7 @@ class ChartManager {
Plotly.restyle(plotId, candlestickUpdate, [0]);
Plotly.restyle(plotId, volumeUpdate, [1]);
console.log(`Updated ${timeframe} chart at ${new Date().toLocaleTimeString()}`);
console.log(`Updated ${timeframe} chart with ${chartData.timestamps.length} candles at ${new Date().toLocaleTimeString()}`);
}
} catch (error) {
console.error(`Error updating ${timeframe} chart:`, error);
@@ -546,9 +548,9 @@ class ChartManager {
plot_bgcolor: '#1f2937',
paper_bgcolor: '#1f2937',
font: { color: '#f8f9fa', size: 11 },
margin: { l: 60, r: 20, t: 10, b: 40 },
margin: { l: 80, r: 20, t: 10, b: 40 }, // Increased left margin for better Y-axis drag area
hovermode: 'x unified',
dragmode: 'pan',
dragmode: 'pan', // Pan mode for main chart area (horizontal panning)
// Performance optimizations
autosize: true,
staticPlot: false
@@ -562,7 +564,7 @@ class ChartManager {
scrollZoom: true,
// Performance optimizations
doubleClick: 'reset', // Enable double-click reset
showAxisDragHandles: true, // Enable axis dragging
showAxisDragHandles: true, // Enable axis dragging - allows Y-axis vertical zoom when dragging on Y-axis area
showAxisRangeEntryBoxes: false
};
@@ -711,6 +713,10 @@ class ChartManager {
Plotly.newPlot(plotId, chartData, layout, config).then(() => {
// Optimize rendering after initial plot
plotElement._fullLayout._replotting = false;
// Add custom handler for Y-axis vertical zoom
// When user drags on Y-axis area (left side), enable vertical zoom
this._setupYAxisZoom(plotElement, plotId, timeframe);
});
// Store chart reference
@@ -777,6 +783,134 @@ class ChartManager {
console.log(`Chart created for ${timeframe} with ${data.timestamps.length} candles`);
}
/**
* Setup Y-axis vertical zoom handler
* Allows vertical zoom when dragging on the Y-axis area (left side of chart)
*/
_setupYAxisZoom(plotElement, plotId, timeframe) {
let isDraggingYAxis = false;
let dragStartY = null;
let dragStartRange = null;
const Y_AXIS_MARGIN = 80; // Left margin width in pixels
// Mouse down handler - check if on Y-axis area
const handleMouseDown = (event) => {
const rect = plotElement.getBoundingClientRect();
const x = event.clientX - rect.left;
// Check if click is in Y-axis area (left margin)
if (x < Y_AXIS_MARGIN) {
isDraggingYAxis = true;
dragStartY = event.clientY;
// Get current Y-axis range
const layout = plotElement._fullLayout;
if (layout && layout.yaxis && layout.yaxis.range) {
dragStartRange = {
min: layout.yaxis.range[0],
max: layout.yaxis.range[1],
range: layout.yaxis.range[1] - layout.yaxis.range[0]
};
}
// Change cursor to indicate vertical zoom
plotElement.style.cursor = 'ns-resize';
event.preventDefault();
event.stopPropagation();
}
};
// Mouse move handler - handle vertical zoom and cursor update
const handleMouseMove = (event) => {
const rect = plotElement.getBoundingClientRect();
const x = event.clientX - rect.left;
// Update cursor when hovering over Y-axis area (only if not dragging)
if (!isDraggingYAxis) {
if (x < Y_AXIS_MARGIN) {
plotElement.style.cursor = 'ns-resize';
} else {
plotElement.style.cursor = 'default';
}
}
// Handle vertical zoom drag
if (isDraggingYAxis && dragStartY !== null && dragStartRange !== null) {
const deltaY = dragStartY - event.clientY; // Negative = zoom in (drag up), Positive = zoom out (drag down)
const zoomFactor = 1 + (deltaY / 200); // Adjust sensitivity (200px = 2x zoom)
// Clamp zoom factor to reasonable limits
const clampedZoom = Math.max(0.1, Math.min(10, zoomFactor));
// Calculate new range centered on current view
const center = (dragStartRange.min + dragStartRange.max) / 2;
const newRange = dragStartRange.range * clampedZoom;
const newMin = center - newRange / 2;
const newMax = center + newRange / 2;
// Update Y-axis range
Plotly.relayout(plotId, {
'yaxis.range': [newMin, newMax]
});
event.preventDefault();
event.stopPropagation();
}
};
// Mouse up handler - end drag (use document level to catch even if mouse leaves element)
const handleMouseUp = () => {
if (isDraggingYAxis) {
isDraggingYAxis = false;
dragStartY = null;
dragStartRange = null;
plotElement.style.cursor = 'default';
}
};
// Mouse leave handler - reset cursor but keep dragging state
const handleMouseLeave = () => {
if (!isDraggingYAxis) {
plotElement.style.cursor = 'default';
}
};
// Attach event listeners
// Use element-level for mousedown and mouseleave (hover detection)
plotElement.addEventListener('mousedown', handleMouseDown);
plotElement.addEventListener('mouseleave', handleMouseLeave);
plotElement.addEventListener('mousemove', handleMouseMove);
// Use document-level for mousemove and mouseup during drag (works even if mouse leaves element)
const handleDocumentMouseMove = (event) => {
if (isDraggingYAxis) {
handleMouseMove(event);
}
};
const handleDocumentMouseUp = () => {
if (isDraggingYAxis) {
handleMouseUp();
}
};
document.addEventListener('mousemove', handleDocumentMouseMove);
document.addEventListener('mouseup', handleDocumentMouseUp);
// Store handlers for cleanup if needed
if (!plotElement._yAxisZoomHandlers) {
plotElement._yAxisZoomHandlers = {
mousedown: handleMouseDown,
mousemove: handleMouseMove,
mouseleave: handleMouseLeave,
documentMousemove: handleDocumentMouseMove,
documentMouseup: handleDocumentMouseUp
};
}
console.log(`[${timeframe}] Y-axis vertical zoom enabled - drag on left side (Y-axis area) to zoom vertically`);
}
/**
* Handle chart click for annotation
@@ -2081,6 +2215,12 @@ class ChartManager {
};
validatedCount++;
// Calculate prediction range vs actual range to diagnose "wide" predictions
const predRange = predCandle[1] - predCandle[2]; // High - Low
const actualRange = actualCandle[1] - actualCandle[2];
const rangeRatio = predRange / actualRange; // >1 means prediction is wider
console.log(`[${timeframe}] Prediction validated (#${validatedCount}):`, {
timestamp: prediction.timestamp,
matchedTo: timestamps[matchIdx],
@@ -2090,34 +2230,144 @@ class ChartManager {
volumeError: pctErrors.volume.toFixed(2) + '%',
direction: directionCorrect ? '✓' : '✗',
timeDiff: Math.abs(predTime - new Date(timestamps[matchIdx]).getTime()) + 'ms',
rangeAnalysis: {
predictedRange: predRange.toFixed(2),
actualRange: actualRange.toFixed(2),
rangeRatio: rangeRatio.toFixed(2) + 'x', // Shows if prediction is wider
isWider: rangeRatio > 1.2 ? 'YES (too wide)' : rangeRatio < 0.8 ? 'NO (too narrow)' : 'OK'
},
predicted: {
O: predCandle[0].toFixed(2),
H: predCandle[1].toFixed(2),
L: predCandle[2].toFixed(2),
C: predCandle[3].toFixed(2),
V: predCandle[4].toFixed(2)
V: predCandle[4].toFixed(2),
Range: predRange.toFixed(2)
},
actual: {
O: actualCandle[0].toFixed(2),
H: actualCandle[1].toFixed(2),
L: actualCandle[2].toFixed(2),
C: actualCandle[3].toFixed(2),
V: actualCandle[4].toFixed(2)
V: actualCandle[4].toFixed(2),
Range: actualRange.toFixed(2)
}
});
// Send metrics to backend for training feedback
this._sendPredictionMetrics(timeframe, prediction);
// Update overall model accuracy metrics
this._updateModelAccuracyMetrics(timeframe, accuracy, directionCorrect);
}
});
// Summary log
if (validatedCount > 0) {
const totalPending = predictions.filter(p => !p.accuracy).length;
const avgAccuracy = this.modelAccuracyMetrics[timeframe]?.avgAccuracy || 0;
const directionAccuracy = this.modelAccuracyMetrics[timeframe]?.directionAccuracy || 0;
console.log(`[${timeframe}] Validated ${validatedCount} predictions, ${totalPending} still pending`);
console.log(`[${timeframe}] Model Accuracy: ${avgAccuracy.toFixed(1)}% avg, ${directionAccuracy.toFixed(1)}% direction`);
// CRITICAL: Re-render predictions to show updated accuracy in tooltips
// Trigger a refresh of prediction display
this._refreshPredictionDisplay(timeframe);
}
}
/**
* Update overall model accuracy metrics
*/
_updateModelAccuracyMetrics(timeframe, accuracy, directionCorrect) {
if (!this.modelAccuracyMetrics[timeframe]) {
this.modelAccuracyMetrics[timeframe] = {
accuracies: [],
directionCorrect: [],
totalValidated: 0
};
}
const metrics = this.modelAccuracyMetrics[timeframe];
metrics.accuracies.push(accuracy);
metrics.directionCorrect.push(directionCorrect);
metrics.totalValidated++;
// Calculate averages
metrics.avgAccuracy = metrics.accuracies.reduce((a, b) => a + b, 0) / metrics.accuracies.length;
metrics.directionAccuracy = (metrics.directionCorrect.filter(c => c).length / metrics.directionCorrect.length) * 100;
// Keep only last 100 validations for rolling average
if (metrics.accuracies.length > 100) {
metrics.accuracies = metrics.accuracies.slice(-100);
metrics.directionCorrect = metrics.directionCorrect.slice(-100);
}
}
/**
* Refresh prediction display to show updated accuracy
*/
_refreshPredictionDisplay(timeframe) {
const chart = this.charts[timeframe];
if (!chart) return;
const plotId = chart.plotId;
const plotElement = document.getElementById(plotId);
if (!plotElement) return;
// Get current predictions from history
if (!this.ghostCandleHistory[timeframe] || this.ghostCandleHistory[timeframe].length === 0) {
return;
}
// Rebuild prediction traces with updated accuracy
const predictionTraces = [];
for (const ghost of this.ghostCandleHistory[timeframe]) {
this._addGhostCandlePrediction(ghost.candle, timeframe, predictionTraces, ghost.targetTime, ghost.accuracy);
}
// Remove old prediction traces
const currentTraces = plotElement.data.length;
const indicesToRemove = [];
for (let i = currentTraces - 1; i >= 0; i--) {
const name = plotElement.data[i].name;
if (name === 'Ghost Prediction' || name === 'Shadow Prediction') {
indicesToRemove.push(i);
}
}
if (indicesToRemove.length > 0) {
Plotly.deleteTraces(plotId, indicesToRemove);
}
// Add updated traces
if (predictionTraces.length > 0) {
Plotly.addTraces(plotId, predictionTraces);
console.log(`[${timeframe}] Refreshed ${predictionTraces.length} prediction candles with updated accuracy`);
}
}
/**
* Get overall model accuracy metrics for a timeframe
*/
getModelAccuracyMetrics(timeframe) {
if (!this.modelAccuracyMetrics[timeframe]) {
return {
avgAccuracy: 0,
directionAccuracy: 0,
totalValidated: 0,
recentAccuracies: []
};
}
const metrics = this.modelAccuracyMetrics[timeframe];
return {
avgAccuracy: metrics.avgAccuracy || 0,
directionAccuracy: metrics.directionAccuracy || 0,
totalValidated: metrics.totalValidated || 0,
recentAccuracies: metrics.accuracies.slice(-10) || [] // Last 10 accuracies
};
}
/**
* Send prediction accuracy metrics to backend for training feedback
*/
@@ -2814,6 +3064,169 @@ class ChartManager {
}
}
/**
* Add executed trade marker to chart
* Shows entry/exit points, PnL, and position lines
*/
addExecutedTradeMarker(trade, positionState) {
try {
if (!trade || !trade.timestamp) return;
// Find which timeframe to display on (prefer 1m, fallback to 1s)
const timeframe = this.timeframes.includes('1m') ? '1m' : (this.timeframes.includes('1s') ? '1s' : null);
if (!timeframe) return;
const chart = this.charts[timeframe];
if (!chart) return;
const plotId = chart.plotId;
const plotElement = document.getElementById(plotId);
if (!plotElement) return;
// Parse timestamp
const timestamp = new Date(trade.timestamp);
const year = timestamp.getUTCFullYear();
const month = String(timestamp.getUTCMonth() + 1).padStart(2, '0');
const day = String(timestamp.getUTCDate()).padStart(2, '0');
const hours = String(timestamp.getUTCHours()).padStart(2, '0');
const minutes = String(timestamp.getUTCMinutes()).padStart(2, '0');
const seconds = String(timestamp.getUTCSeconds()).padStart(2, '0');
const formattedTimestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
// Determine action type and styling
let shape, annotation;
if (trade.action === 'OPEN_LONG') {
// Green upward arrow for long entry
shape = {
type: 'line',
x0: formattedTimestamp,
x1: formattedTimestamp,
y0: trade.price * 0.997,
y1: trade.price * 0.993,
line: { color: '#10b981', width: 3 },
name: `trade_${trade.trade_id}`
};
annotation = {
x: formattedTimestamp,
y: trade.price * 0.992,
text: `LONG<br>$${trade.price.toFixed(2)}`,
showarrow: true,
arrowhead: 2,
arrowcolor: '#10b981',
ax: 0,
ay: 30,
font: { size: 10, color: '#10b981', weight: 'bold' },
bgcolor: 'rgba(16, 185, 129, 0.2)'
};
} else if (trade.action === 'OPEN_SHORT') {
// Red downward arrow for short entry
shape = {
type: 'line',
x0: formattedTimestamp,
x1: formattedTimestamp,
y0: trade.price * 1.003,
y1: trade.price * 1.007,
line: { color: '#ef4444', width: 3 },
name: `trade_${trade.trade_id}`
};
annotation = {
x: formattedTimestamp,
y: trade.price * 1.008,
text: `SHORT<br>$${trade.price.toFixed(2)}`,
showarrow: true,
arrowhead: 2,
arrowcolor: '#ef4444',
ax: 0,
ay: -30,
font: { size: 10, color: '#ef4444', weight: 'bold' },
bgcolor: 'rgba(239, 68, 68, 0.2)'
};
} else if (trade.action === 'CLOSE_LONG' || trade.action === 'CLOSE_SHORT') {
// Exit marker with PnL
const isProfit = trade.pnl > 0;
const color = isProfit ? '#10b981' : '#ef4444';
const positionType = trade.action === 'CLOSE_LONG' ? 'LONG' : 'SHORT';
shape = {
type: 'line',
x0: formattedTimestamp,
x1: formattedTimestamp,
y0: trade.price,
y1: trade.price,
line: { color: color, width: 4, dash: 'dot' },
name: `trade_${trade.trade_id}_exit`
};
annotation = {
x: formattedTimestamp,
y: trade.price,
text: `EXIT ${positionType}<br>$${trade.price.toFixed(2)}<br>PnL: ${isProfit ? '+' : ''}$${trade.pnl.toFixed(2)} (${trade.pnl_pct >= 0 ? '+' : ''}${trade.pnl_pct.toFixed(2)}%)`,
showarrow: true,
arrowhead: 1,
arrowcolor: color,
ax: 0,
ay: isProfit ? -40 : 40,
font: { size: 10, color: color, weight: 'bold' },
bgcolor: isProfit ? 'rgba(16, 185, 129, 0.3)' : 'rgba(239, 68, 68, 0.3)'
};
// Add position line connecting entry to exit if entry time available
if (trade.entry_time) {
const entryTimestamp = new Date(trade.entry_time);
const entryYear = entryTimestamp.getUTCFullYear();
const entryMonth = String(entryTimestamp.getUTCMonth() + 1).padStart(2, '0');
const entryDay = String(entryTimestamp.getUTCDate()).padStart(2, '0');
const entryHours = String(entryTimestamp.getUTCHours()).padStart(2, '0');
const entryMinutes = String(entryTimestamp.getUTCMinutes()).padStart(2, '0');
const entrySeconds = String(entryTimestamp.getUTCSeconds()).padStart(2, '0');
const formattedEntryTime = `${entryYear}-${entryMonth}-${entryDay} ${entryHours}:${entryMinutes}:${entrySeconds}`;
const positionLine = {
type: 'rect',
x0: formattedEntryTime,
x1: formattedTimestamp,
y0: trade.entry_price,
y1: trade.price,
fillcolor: isProfit ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)',
line: { color: color, width: 2, dash: isProfit ? 'solid' : 'dash' },
name: `position_${trade.trade_id}`
};
// Add both position rectangle and exit marker
const currentShapes = plotElement.layout.shapes || [];
Plotly.relayout(plotId, {
shapes: [...currentShapes, positionLine, shape]
});
} else {
// Just add exit marker
const currentShapes = plotElement.layout.shapes || [];
Plotly.relayout(plotId, {
shapes: [...currentShapes, shape]
});
}
} else {
// Entry marker only (no position line yet)
const currentShapes = plotElement.layout.shapes || [];
Plotly.relayout(plotId, {
shapes: [...currentShapes, shape]
});
}
// Add annotation
if (annotation) {
const currentAnnotations = plotElement.layout.annotations || [];
Plotly.relayout(plotId, {
annotations: [...currentAnnotations, annotation]
});
}
console.log(`Added executed trade marker: ${trade.action} @ ${trade.price.toFixed(2)}`);
} catch (error) {
console.error('Error adding executed trade marker:', error);
}
}
/**
* Remove live metrics overlay
*/