diff --git a/ANNOTATE/core/real_training_adapter.py b/ANNOTATE/core/real_training_adapter.py index b5c7baf..eab4cfa 100644 --- a/ANNOTATE/core/real_training_adapter.py +++ b/ANNOTATE/core/real_training_adapter.py @@ -570,62 +570,6 @@ class RealTrainingAdapter: except Exception as e: logger.debug(f" {timeframe}: Replay failed: {e}") - # CRITICAL FIX: For 1s timeframe, if we still don't have enough data, generate from 1m candles - if timeframe == '1s' and (df is None or df.empty or len(df) < min_required_candles): - try: - df_1m = None - - # First, try to get 1m data from already fetched timeframes - if '1m' in fetched_timeframes and '1m' in market_state.get('timeframes', {}): - # Convert dict format to DataFrame - tf_1m = market_state['timeframes']['1m'] - try: - df_1m = pd.DataFrame({ - 'open': tf_1m['open'], - 'high': tf_1m['high'], - 'low': tf_1m['low'], - 'close': tf_1m['close'], - 'volume': tf_1m['volume'] - }, index=pd.to_datetime(tf_1m['timestamps'], utc=True)) - except Exception as e: - logger.debug(f" {timeframe}: Could not convert 1m dict to DataFrame: {e}") - - # If we don't have 1m data yet, fetch it - if df_1m is None or df_1m.empty: - logger.debug(f" {timeframe}: Fetching 1m data to generate 1s candles...") - if duckdb_storage: - try: - df_1m = duckdb_storage.get_ohlcv_data( - symbol=symbol, - timeframe='1m', - start_time=tf_start_time, - end_time=tf_end_time, - limit=limit, - direction='before' - ) - except Exception as e: - logger.debug(f" {timeframe}: Could not get 1m from DuckDB: {e}") - - if df_1m is None or df_1m.empty: - # Try API for 1m - df_1m = self._fetch_historical_from_api(symbol, '1m', tf_start_time, tf_end_time, limit) - - # Generate 1s candles from 1m if we have 1m data - if df_1m is not None and not df_1m.empty: - # Generate 1s candles from 1m - generated_1s = self._generate_1s_from_1m(df_1m, min_required_candles) - if generated_1s is not None and len(generated_1s) >= min_required_candles: - df = generated_1s - logger.info(f" {timeframe}: Generated {len(df)} candles from {len(df_1m)} 1m candles") - else: - logger.warning(f" {timeframe}: Generated only {len(generated_1s) if generated_1s is not None else 0} candles from 1m (need {min_required_candles})") - else: - logger.debug(f" {timeframe}: No 1m data available to generate 1s candles") - except Exception as e: - logger.warning(f" {timeframe}: Failed to generate from 1m: {e}") - import traceback - logger.debug(traceback.format_exc()) - # Validate data quality before storing if df is not None and not df.empty: # Check minimum candle count @@ -1446,122 +1390,6 @@ class RealTrainingAdapter: state_size = agent.state_size if hasattr(agent, 'state_size') else 100 return [0.0] * state_size - def _generate_1s_from_1m(self, df_1m: pd.DataFrame, min_candles: int) -> Optional[pd.DataFrame]: - """ - Generate 1s candles from 1m candles by splitting each 1m candle into 60 1s candles. - - Args: - df_1m: DataFrame with 1m OHLCV data - min_candles: Minimum number of 1s candles to generate - - Returns: - DataFrame with 1s OHLCV data or None if generation fails - """ - import pandas as pd - from datetime import timedelta - - try: - if df_1m is None or df_1m.empty: - return None - - # Ensure we have required columns - required_cols = ['open', 'high', 'low', 'close', 'volume'] - if not all(col in df_1m.columns for col in required_cols): - logger.warning("1m DataFrame missing required columns for 1s generation") - return None - - # Generate 1s candles from each 1m candle - # Each 1m candle becomes 60 1s candles - candles_1s = [] - - for idx, row in df_1m.iterrows(): - # Get timestamp (handle both index and column) - if isinstance(df_1m.index, pd.DatetimeIndex): - timestamp = idx - elif 'timestamp' in df_1m.columns: - timestamp = pd.to_datetime(row['timestamp']) - else: - logger.warning("Cannot determine timestamp for 1m candle") - continue - - # Extract OHLCV values - open_price = float(row['open']) - high_price = float(row['high']) - low_price = float(row['low']) - close_price = float(row['close']) - volume = float(row['volume']) - - # Calculate price change per second (linear interpolation) - price_change = close_price - open_price - price_per_second = price_change / 60.0 - - # Volume per second (distributed evenly) - volume_per_second = volume / 60.0 - - # Generate 60 1s candles from this 1m candle - for second in range(60): - # Calculate timestamp for this second - candle_time = timestamp + timedelta(seconds=second) - - # Interpolate price (linear from open to close) - if second == 0: - candle_open = open_price - candle_close = open_price + price_per_second - elif second == 59: - candle_open = open_price + (price_per_second * 59) - candle_close = close_price - else: - candle_open = open_price + (price_per_second * second) - candle_close = open_price + (price_per_second * (second + 1)) - - # High and low: use the interpolated prices, but ensure they stay within the 1m candle's range - # Add small random variation to make it more realistic (but keep within bounds) - candle_high = max(candle_open, candle_close) - candle_low = min(candle_open, candle_close) - - # Ensure high/low don't exceed the 1m candle's range - candle_high = min(candle_high, high_price) - candle_low = max(candle_low, low_price) - - # If high == low, add small spread - if candle_high == candle_low: - spread = (high_price - low_price) / 60.0 - candle_high = candle_high + (spread * 0.5) - candle_low = candle_low - (spread * 0.5) - - candles_1s.append({ - 'timestamp': candle_time, - 'open': candle_open, - 'high': candle_high, - 'low': candle_low, - 'close': candle_close, - 'volume': volume_per_second - }) - - if not candles_1s: - return None - - # Convert to DataFrame - df_1s = pd.DataFrame(candles_1s) - df_1s['timestamp'] = pd.to_datetime(df_1s['timestamp']) - df_1s = df_1s.set_index('timestamp') - - # Sort by timestamp - df_1s = df_1s.sort_index() - - # Take the most recent candles up to the limit - if len(df_1s) > min_candles: - df_1s = df_1s.tail(min_candles) - - logger.debug(f"Generated {len(df_1s)} 1s candles from {len(df_1m)} 1m candles") - return df_1s - - except Exception as e: - logger.warning(f"Error generating 1s candles from 1m: {e}") - import traceback - logger.debug(traceback.format_exc()) - return None - def _fetch_historical_from_api(self, symbol: str, timeframe: str, start_time: datetime, end_time: datetime, limit: int) -> Optional[pd.DataFrame]: """ Fetch historical OHLCV data from exchange APIs for a specific time range @@ -3288,34 +3116,13 @@ class RealTrainingAdapter: logger.info(f"Stopped real-time inference: {inference_id}") def get_latest_signals(self, limit: int = 50) -> List[Dict]: - """Get latest inference signals from orchestrator predictions and active sessions""" + """Get latest inference signals from all active sessions""" + if not hasattr(self, 'inference_sessions'): + return [] + all_signals = [] - - # CRITICAL FIX: Get signals from orchestrator's stored predictions (primary source) - if self.orchestrator and hasattr(self.orchestrator, 'recent_transformer_predictions'): - # Get predictions for all symbols - for symbol, predictions in self.orchestrator.recent_transformer_predictions.items(): - if predictions: - # Convert predictions to signal format - for pred in list(predictions)[-limit:]: - signal = { - 'timestamp': pred.get('timestamp', datetime.now(timezone.utc).isoformat()), - 'action': pred.get('action', 'HOLD'), - 'confidence': pred.get('confidence', 0.0), - 'current_price': pred.get('current_price', 0.0), - 'price': pred.get('current_price', 0.0), # Alias for compatibility - 'predicted_price': pred.get('predicted_price', pred.get('current_price', 0.0)), - 'price_change': pred.get('price_change', 0.0), - 'model': 'transformer', - 'predicted_candle': pred.get('predicted_candle', {}), - 'source': pred.get('source', 'live_inference') - } - all_signals.append(signal) - - # Also get signals from active inference sessions (secondary source) - if hasattr(self, 'inference_sessions'): - for session in self.inference_sessions.values(): - all_signals.extend(session.get('signals', [])) + for session in self.inference_sessions.values(): + all_signals.extend(session.get('signals', [])) # Sort by timestamp and return latest all_signals.sort(key=lambda x: x.get('timestamp', ''), reverse=True) diff --git a/ANNOTATE/web/app.py b/ANNOTATE/web/app.py index 3f0d9be..c43042b 100644 --- a/ANNOTATE/web/app.py +++ b/ANNOTATE/web/app.py @@ -1189,7 +1189,7 @@ class AnnotationDashboard: # Add ALL pivot points to the map for pivot in trend_level.pivot_points: - ts_str = pivot.timestamp.strftime('%Y-%m-%d %H:%M:%S') + ts_str = self._format_timestamp_utc(pivot.timestamp) if ts_str not in pivot_map: pivot_map[ts_str] = {'highs': [], 'lows': []} @@ -1226,6 +1226,51 @@ class AnnotationDashboard: logger.error(traceback.format_exc()) return {} + def _format_timestamp_utc(self, ts): + """ + Format timestamp in ISO format with UTC indicator ('Z' suffix) + This ensures frontend JavaScript parses it as UTC, not local time + + Args: + ts: pandas Timestamp or datetime object + + Returns: + str: ISO format timestamp with 'Z' suffix (e.g., '2025-12-08T21:00:00Z') + """ + try: + # Ensure timestamp is UTC + if hasattr(ts, 'tz'): + if ts.tz is not None: + ts_utc = ts.tz_convert('UTC') if hasattr(ts, 'tz_convert') else ts + else: + try: + ts_utc = ts.tz_localize('UTC') if hasattr(ts, 'tz_localize') else ts + except: + ts_utc = ts + else: + ts_utc = ts + + # Format as ISO with 'Z' for UTC + if hasattr(ts_utc, 'strftime'): + return ts_utc.strftime('%Y-%m-%dT%H:%M:%SZ') + else: + return str(ts_utc) + except Exception as e: + logger.debug(f"Error formatting timestamp: {e}") + return str(ts) + + def _format_timestamps_utc(self, timestamp_series): + """ + Format a series of timestamps in ISO format with UTC indicator + + Args: + timestamp_series: pandas Index or Series with timestamps + + Returns: + list: List of ISO format timestamps with 'Z' suffix + """ + return [self._format_timestamp_utc(ts) for ts in timestamp_series] + def _setup_routes(self): """Setup Flask routes""" @@ -1418,7 +1463,7 @@ class AnnotationDashboard: chart_data = { timeframe: { - 'timestamps': df.index.strftime('%Y-%m-%d %H:%M:%S').tolist(), + 'timestamps': self._format_timestamps_utc(df.index), 'open': df['open'].tolist(), 'high': df['high'].tolist(), 'low': df['low'].tolist(), @@ -1496,7 +1541,7 @@ class AnnotationDashboard: # Convert to format suitable for Plotly chart_data[timeframe] = { - 'timestamps': df.index.strftime('%Y-%m-%d %H:%M:%S').tolist(), + 'timestamps': self._format_timestamps_utc(df.index), 'open': df['open'].tolist(), 'high': df['high'].tolist(), 'low': df['low'].tolist(), @@ -1766,8 +1811,31 @@ class AnnotationDashboard: # Get pivot markers for this timeframe pivot_markers = self._get_pivot_markers_for_timeframe(symbol, timeframe, df) + # CRITICAL FIX: Format timestamps in ISO format with UTC indicator + # This ensures frontend parses them as UTC, not local time + timestamps = [] + for ts in df.index: + # Ensure timestamp is UTC + if hasattr(ts, 'tz'): + if ts.tz is not None: + ts_utc = ts.tz_convert('UTC') if hasattr(ts, 'tz_convert') else ts + else: + try: + ts_utc = ts.tz_localize('UTC') if hasattr(ts, 'tz_localize') else ts + except: + ts_utc = ts + else: + ts_utc = ts + + # Format as ISO with 'Z' for UTC: 'YYYY-MM-DDTHH:MM:SSZ' + # Plotly handles ISO format correctly + if hasattr(ts_utc, 'strftime'): + timestamps.append(ts_utc.strftime('%Y-%m-%dT%H:%M:%SZ')) + else: + timestamps.append(str(ts_utc)) + chart_data[timeframe] = { - 'timestamps': df.index.strftime('%Y-%m-%d %H:%M:%S').tolist(), + 'timestamps': timestamps, 'open': df['open'].tolist(), 'high': df['high'].tolist(), 'low': df['low'].tolist(), @@ -2590,6 +2658,30 @@ class AnnotationDashboard: metrics = session['metrics'].copy() break + # CRITICAL FIX: Include position state and session metrics for UI state restoration + position_state = None + session_metrics = None + + # Get position state and session metrics from orchestrator if available + if self.orchestrator and hasattr(self.orchestrator, 'get_position_state'): + try: + position_state = self.orchestrator.get_position_state() + except: + pass + + if self.orchestrator and hasattr(self.orchestrator, 'get_session_metrics'): + try: + session_metrics = self.orchestrator.get_session_metrics() + except: + pass + + # Add position state and session metrics to metrics dict + if position_state: + metrics['position_state'] = position_state + if session_metrics: + metrics['session_pnl'] = session_metrics.get('total_pnl', 0.0) + metrics['session_metrics'] = session_metrics + return jsonify({ 'success': True, 'signals': signals, @@ -3106,7 +3198,7 @@ class AnnotationDashboard: if not df_before.empty: recent = df_before.tail(200) market_state['timeframes'][tf] = { - 'timestamps': recent.index.strftime('%Y-%m-%d %H:%M:%S').tolist(), + 'timestamps': self._format_timestamps_utc(recent.index), 'open': recent['open'].tolist(), 'high': recent['high'].tolist(), 'low': recent['low'].tolist(), diff --git a/ANNOTATE/web/static/js/chart_manager.js b/ANNOTATE/web/static/js/chart_manager.js index cd143c2..cc28b36 100644 --- a/ANNOTATE/web/static/js/chart_manager.js +++ b/ANNOTATE/web/static/js/chart_manager.js @@ -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
Price: $${pivot.price.toFixed(2)}
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
Price: $${pivot.price.toFixed(2)}
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
Price: $${pivot.price.toFixed(2)}
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
Price: $${pivot.price.toFixed(2)}
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
Price: ${pivot.price.toFixed(2)}
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
Price: ${pivot.price.toFixed(2)}
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)`); } /** diff --git a/ANNOTATE/web/templates/annotation_dashboard.html b/ANNOTATE/web/templates/annotation_dashboard.html index 8d97844..0420e0c 100644 --- a/ANNOTATE/web/templates/annotation_dashboard.html +++ b/ANNOTATE/web/templates/annotation_dashboard.html @@ -102,6 +102,11 @@ checkActiveTraining(); } + // Check for active inference session (resume PnL and position state after page reload) + if (typeof checkActiveInference === 'function') { + checkActiveInference(); + } + // Keyboard shortcuts for chart maximization document.addEventListener('keydown', function(e) { // ESC key to exit maximized mode diff --git a/ANNOTATE/web/templates/components/training_panel.html b/ANNOTATE/web/templates/components/training_panel.html index 1b9264f..6a76151 100644 --- a/ANNOTATE/web/templates/components/training_panel.html +++ b/ANNOTATE/web/templates/components/training_panel.html @@ -234,6 +234,24 @@ activeTrainingId = data.session.training_id; showTrainingStatus(); + // CRITICAL FIX: Immediately restore training progress state + // Don't wait for first poll - restore current state now + if (data.session.current_epoch !== undefined) { + document.getElementById('training-epoch').textContent = data.session.current_epoch || 0; + } + if (data.session.total_epochs !== undefined) { + document.getElementById('training-total-epochs').textContent = data.session.total_epochs || 0; + } + if (data.session.current_loss !== undefined && data.session.current_loss !== null) { + document.getElementById('training-loss').textContent = data.session.current_loss.toFixed(4); + } + + // Update progress bar immediately + if (data.session.current_epoch && data.session.total_epochs) { + const percentage = (data.session.current_epoch / data.session.total_epochs) * 100; + document.getElementById('training-progress-bar').style.width = percentage + '%'; + } + // Populate annotation count and timeframe if available if (data.session.annotation_count) { document.getElementById('training-annotation-count').textContent = data.session.annotation_count; @@ -242,6 +260,7 @@ document.getElementById('training-timeframe').textContent = data.session.timeframe.toUpperCase(); } + // Start polling for continued updates (will update GPU/CPU and future progress) pollTrainingProgress(activeTrainingId); } else { console.log('No active training session'); @@ -252,6 +271,62 @@ }); } + function checkActiveInference() { + /** + * Check if there's an active real-time inference session on page load + * This allows resuming PnL tracking and position state after page reload + */ + fetch('/api/realtime-inference/signals') + .then(response => response.json()) + .then(data => { + if (data.success) { + // Check if inference is active (signals endpoint returns data if active) + if (data.signals && data.signals.length > 0) { + console.log('Active inference session found, restoring state'); + + // Restore PnL and position state from metrics + if (data.metrics) { + // Update session PnL if available + if (data.metrics.session_pnl !== undefined) { + const sessionPnlEl = document.getElementById('session-pnl'); + if (sessionPnlEl) { + const totalPnl = data.metrics.session_pnl || 0; + const pnlColor = totalPnl >= 0 ? 'text-success' : 'text-danger'; + const pnlSign = totalPnl >= 0 ? '+' : ''; + sessionPnlEl.textContent = `${pnlSign}$${totalPnl.toFixed(2)}`; + sessionPnlEl.className = `fw-bold ${pnlColor}`; + } + } + + // Update position state if available + if (data.metrics.position_state) { + updatePositionStateDisplay(data.metrics.position_state, data.metrics.session_metrics || data.metrics); + } + } + + // Restore live metrics (accuracy, loss) if available + if (data.metrics) { + if (data.metrics.accuracy !== undefined) { + const liveAccuracyEl = document.getElementById('live-accuracy'); + if (liveAccuracyEl) { + liveAccuracyEl.textContent = (data.metrics.accuracy * 100).toFixed(1) + '%'; + } + } + if (data.metrics.loss !== undefined) { + const liveLossEl = document.getElementById('live-loss'); + if (liveLossEl) { + liveLossEl.textContent = data.metrics.loss.toFixed(4); + } + } + } + } + } + }) + .catch(error => { + console.error('Error checking active inference:', error); + }); + } + function loadAvailableModels() { fetch('/api/available-models') .then(response => response.json())