From 4b93b6fd4217b72c641a6dd04553c28a5ef5900a Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sat, 22 Nov 2025 18:23:04 +0200 Subject: [PATCH] fix 1s 1m chart less candles ; fix vertical zoom --- ANNOTATE/core/real_training_adapter.py | 211 ++++++++++- ANNOTATE/web/app.py | 5 + ANNOTATE/web/static/js/chart_manager.js | 427 +++++++++++++++++++++- ANNOTATE/web/static/js/live_updates_ws.js | 32 ++ 4 files changed, 664 insertions(+), 11 deletions(-) diff --git a/ANNOTATE/core/real_training_adapter.py b/ANNOTATE/core/real_training_adapter.py index 34f9285..0de8443 100644 --- a/ANNOTATE/core/real_training_adapter.py +++ b/ANNOTATE/core/real_training_adapter.py @@ -2430,13 +2430,14 @@ class RealTrainingAdapter: if not hasattr(self, 'inference_sessions'): self.inference_sessions = {} - # Create inference session + # Create inference session with position tracking self.inference_sessions[inference_id] = { 'model_name': model_name, 'symbol': symbol, 'status': 'running', 'start_time': time.time(), - 'signals': [], + 'signals': [], # All signals (including rejected ones) + 'executed_trades': [], # Only executed trades (open/close positions) 'stop_flag': False, 'live_training_enabled': enable_live_training, 'train_every_candle': train_every_candle, @@ -2447,7 +2448,13 @@ class RealTrainingAdapter: 'loss': 0.0, 'steps': 0 }, - 'last_candle_time': None + 'last_candle_time': None, + # Position tracking + 'position': None, # {'type': 'long/short', 'entry_price': float, 'entry_time': str, 'entry_id': str} + 'total_pnl': 0.0, + 'win_count': 0, + 'loss_count': 0, + 'total_trades': 0 } training_mode = "per-candle" if train_every_candle else ("pivot-based" if enable_live_training else "inference-only") @@ -3211,13 +3218,39 @@ class RealTrainingAdapter: 'predicted_candle': prediction.get('predicted_candle') } + # Store signal (all signals, including rejected ones) session['signals'].append(signal) # Keep only last 100 signals if len(session['signals']) > 100: session['signals'] = session['signals'][-100:] - logger.info(f"Live Signal: {signal['action']} @ {signal['price']:.2f} (conf: {signal['confidence']:.2f})") + # Execute trade logic (only if confidence is high enough and position logic allows) + executed_trade = self._execute_realtime_trade(session, signal, current_price) + + if executed_trade: + logger.info(f"Live Trade EXECUTED: {executed_trade['action']} @ {executed_trade['price']:.2f} (conf: {signal['confidence']:.2f})") + + # Send executed trade to frontend via WebSocket + if hasattr(self, 'socketio') and self.socketio: + self.socketio.emit('executed_trade', { + 'trade': executed_trade, + 'position_state': { + 'has_position': session['position'] is not None, + 'position_type': session['position']['type'] if session['position'] else None, + 'entry_price': session['position']['entry_price'] if session['position'] else None, + 'unrealized_pnl': self._calculate_unrealized_pnl(session, current_price) if session['position'] else 0.0 + }, + 'session_metrics': { + 'total_pnl': session['total_pnl'], + 'total_trades': session['total_trades'], + 'win_count': session['win_count'], + 'loss_count': session['loss_count'], + 'win_rate': (session['win_count'] / session['total_trades'] * 100) if session['total_trades'] > 0 else 0 + } + }) + else: + logger.info(f"Live Signal (NOT executed): {signal['action']} @ {signal['price']:.2f} (conf: {signal['confidence']:.2f}) - {self._get_rejection_reason(session, signal)}") # Store prediction for visualization if self.orchestrator and hasattr(self.orchestrator, 'store_transformer_prediction'): @@ -3250,3 +3283,173 @@ class RealTrainingAdapter: logger.error(f"Fatal error in inference loop: {e}") session['status'] = 'error' session['error'] = str(e) + + def _execute_realtime_trade(self, session: Dict, signal: Dict, current_price: float) -> Optional[Dict]: + """ + Execute trade based on signal, respecting position management rules + + Rules: + 1. Only execute if confidence >= 0.6 + 2. Only open new position if no position is currently open + 3. Close position on opposite signal + 4. Track all executed trades for visualization + + Returns: + Dict with executed trade info, or None if signal was rejected + """ + action = signal['action'] + confidence = signal['confidence'] + timestamp = signal['timestamp'] + + # Rule 1: Confidence threshold + if confidence < 0.6: + return None # Rejected: low confidence + + # Rule 2 & 3: Position management + position = session.get('position') + + if action == 'BUY': + if position is None: + # Open long position + trade_id = str(uuid.uuid4())[:8] + session['position'] = { + 'type': 'long', + 'entry_price': current_price, + 'entry_time': timestamp, + 'entry_id': trade_id, + 'signal_confidence': confidence + } + + executed_trade = { + 'trade_id': trade_id, + 'action': 'OPEN_LONG', + 'price': current_price, + 'timestamp': timestamp, + 'confidence': confidence + } + + session['executed_trades'].append(executed_trade) + return executed_trade + + elif position['type'] == 'short': + # Close short position + entry_price = position['entry_price'] + pnl = entry_price - current_price # Short profit + pnl_pct = (pnl / entry_price) * 100 + + executed_trade = { + 'trade_id': position['entry_id'], + 'action': 'CLOSE_SHORT', + 'price': current_price, + 'timestamp': timestamp, + 'confidence': confidence, + 'entry_price': entry_price, + 'entry_time': position['entry_time'], + 'pnl': pnl, + 'pnl_pct': pnl_pct + } + + # Update session metrics + session['total_pnl'] += pnl + session['total_trades'] += 1 + if pnl > 0: + session['win_count'] += 1 + else: + session['loss_count'] += 1 + + session['position'] = None + session['executed_trades'].append(executed_trade) + + logger.info(f"Position CLOSED: SHORT @ {current_price:.2f}, PnL=${pnl:.2f} ({pnl_pct:+.2f}%)") + return executed_trade + + elif action == 'SELL': + if position is None: + # Open short position + trade_id = str(uuid.uuid4())[:8] + session['position'] = { + 'type': 'short', + 'entry_price': current_price, + 'entry_time': timestamp, + 'entry_id': trade_id, + 'signal_confidence': confidence + } + + executed_trade = { + 'trade_id': trade_id, + 'action': 'OPEN_SHORT', + 'price': current_price, + 'timestamp': timestamp, + 'confidence': confidence + } + + session['executed_trades'].append(executed_trade) + return executed_trade + + elif position['type'] == 'long': + # Close long position + entry_price = position['entry_price'] + pnl = current_price - entry_price # Long profit + pnl_pct = (pnl / entry_price) * 100 + + executed_trade = { + 'trade_id': position['entry_id'], + 'action': 'CLOSE_LONG', + 'price': current_price, + 'timestamp': timestamp, + 'confidence': confidence, + 'entry_price': entry_price, + 'entry_time': position['entry_time'], + 'pnl': pnl, + 'pnl_pct': pnl_pct + } + + # Update session metrics + session['total_pnl'] += pnl + session['total_trades'] += 1 + if pnl > 0: + session['win_count'] += 1 + else: + session['loss_count'] += 1 + + session['position'] = None + session['executed_trades'].append(executed_trade) + + logger.info(f"Position CLOSED: LONG @ {current_price:.2f}, PnL=${pnl:.2f} ({pnl_pct:+.2f}%)") + return executed_trade + + # HOLD or position already open in same direction + return None + + def _get_rejection_reason(self, session: Dict, signal: Dict) -> str: + """Get reason why a signal was not executed""" + action = signal['action'] + confidence = signal['confidence'] + position = session.get('position') + + if confidence < 0.6: + return f"Low confidence ({confidence:.2f} < 0.6)" + + if action == 'HOLD': + return "HOLD signal (no trade)" + + if position: + if action == 'BUY' and position['type'] == 'long': + return "Already in LONG position" + elif action == 'SELL' and position['type'] == 'short': + return "Already in SHORT position" + + return "Unknown reason" + + def _calculate_unrealized_pnl(self, session: Dict, current_price: float) -> float: + """Calculate unrealized PnL for open position""" + position = session.get('position') + if not position or not current_price: + return 0.0 + + entry_price = position['entry_price'] + + if position['type'] == 'long': + return ((current_price - entry_price) / entry_price) * 100 # Percentage + else: # short + return ((entry_price - current_price) / entry_price) * 100 # Percentage diff --git a/ANNOTATE/web/app.py b/ANNOTATE/web/app.py index 79e13f4..1571736 100644 --- a/ANNOTATE/web/app.py +++ b/ANNOTATE/web/app.py @@ -538,6 +538,9 @@ class AnnotationDashboard: engineio_logger=False ) self.has_socketio = True + # Pass socketio to training adapter for live trade updates + if self.training_adapter: + self.training_adapter.socketio = self.socketio logger.info("SocketIO initialized for real-time updates") except ImportError: self.socketio = None @@ -586,6 +589,8 @@ class AnnotationDashboard: self.annotation_manager = AnnotationManager() # Use REAL training adapter - NO SIMULATION! self.training_adapter = RealTrainingAdapter(None, self.data_provider) + # Pass socketio to training adapter for live trade updates + self.training_adapter.socketio = None # Will be set after socketio initialization # Backtest runner for replaying visible chart with predictions self.backtest_runner = BacktestRunner() diff --git a/ANNOTATE/web/static/js/chart_manager.js b/ANNOTATE/web/static/js/chart_manager.js index 9366b68..a3b5fcf 100644 --- a/ANNOTATE/web/static/js/chart_manager.js +++ b/ANNOTATE/web/static/js/chart_manager.js @@ -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
$${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
$${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}
$${trade.price.toFixed(2)}
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 */ diff --git a/ANNOTATE/web/static/js/live_updates_ws.js b/ANNOTATE/web/static/js/live_updates_ws.js index f2a4468..d6bebc5 100644 --- a/ANNOTATE/web/static/js/live_updates_ws.js +++ b/ANNOTATE/web/static/js/live_updates_ws.js @@ -99,6 +99,18 @@ class LiveUpdatesWebSocket { console.error('Prediction error:', data); }); + this.socket.on('executed_trade', (data) => { + console.log('Executed trade received:', data); + if (this.onExecutedTrade) { + this.onExecutedTrade(data); + } + }); + + this.socket.on('training_update', (data) => { + console.log('Training update received:', data); + // Training feedback from incremental learning + }); + // Error events this.socket.on('connect_error', (error) => { console.error('WebSocket connection error:', error); @@ -230,6 +242,26 @@ document.addEventListener('DOMContentLoaded', function() { } }; + window.liveUpdatesWS.onExecutedTrade = function(data) { + // Visualize executed trade on chart + if (window.appState && window.appState.chartManager) { + window.appState.chartManager.addExecutedTradeMarker(data.trade, data.position_state); + } + + // Update position state display + if (typeof updatePositionStateDisplay === 'function') { + updatePositionStateDisplay(data.position_state, data.session_metrics); + } + + // Log trade details + console.log('Executed Trade:', { + action: data.trade.action, + price: data.trade.price, + pnl: data.trade.pnl ? `$${data.trade.pnl.toFixed(2)} (${data.trade.pnl_pct.toFixed(2)}%)` : 'N/A', + position: data.position_state.has_position ? `${data.position_state.position_type.toUpperCase()} @ $${data.position_state.entry_price}` : 'CLOSED' + }); + }; + // Auto-connect console.log('Auto-connecting to WebSocket...'); window.liveUpdatesWS.connect();