Source code for gui.panels.prediction_panel

"""
Cerebrum Forex - Data-Rich Prediction Panel
Table-centric interface with JSON/CSV access and clear explanations.
"""

import logging
import json
import os
from datetime import datetime, timedelta
from pathlib import Path

from PyQt6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QLabel,
    QTableWidget, QTableWidgetItem, QPushButton, QFrame,
    QTabWidget, QTextEdit, QHeaderView, QAbstractItemView,
    QToolTip, QDoubleSpinBox
)
from PyQt6.QtCore import Qt, QTimer, QUrl, pyqtSignal
from PyQt6.QtGui import QFont, QColor, QDesktopServices

logger = logging.getLogger(__name__)

[docs] class PredictionPanel(QWidget): """ Complete Interface: Prediction Table + JSON + CSV. """ prediction_updated = pyqtSignal(str, str, float) # timeframe, signal, confidence def __init__(self, app_controller=None): super().__init__() self.app_controller = app_controller self._init_ui() # Auto-refresh table every 2 seconds self._timer = QTimer() self._timer.timeout.connect(self._refresh_all) self._timer.start(2000) def _init_ui(self): logger.info("[PredictionPanel] _init_ui START") try: layout = QVBoxLayout(self) layout.setContentsMargins(10, 10, 10, 10) layout.setSpacing(10) # 1. HEADER (Status + Hardware Info) header = QHBoxLayout() title = QLabel("📊 Prediction") title.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold)) title.setStyleSheet("color: #e5e7eb;") header.addWidget(title) # Hardware Tier Badge try: logger.debug("[PredictionPanel] Loading hardware_benchmark...") from core.hardware_benchmark import get_benchmark benchmark = get_benchmark() if benchmark.profile: tier = benchmark.profile.tier.upper() max_1m = benchmark.profile.max_rows_1m // 1000 tier_colors = { "LOW": ("#ef4444", "⚠️"), "MEDIUM": ("#f59e0b", "💻"), "HIGH": ("#10b981", "🚀"), "ULTRA": ("#8b5cf6", "⚡"), } color, icon = tier_colors.get(tier, ("#6b7280", "🖥")) # OVERRIDE: Since we implemented In-Memory 5K Limit, it's effectively ULTRA/FAST for everyone # But we keep the tier correct, just adding the limit info self.hw_badge = QLabel(f"⚡ FAST (Limit: 5K rows)") self.hw_badge.setStyleSheet(f""" background-color: #8b5cf622; color: #a78bfa; padding: 4px 10px; border-radius: 12px; font-size: 10px; font-weight: bold; border: 1px solid #8b5cf6; """) self.hw_badge.setToolTip( f"Hardware Tier: {tier}\n" f"• 1m limited to: {benchmark.profile.max_rows_1m:,} rows\n" f"• 5m limited to: {benchmark.profile.max_rows_5m:,} rows\n" f"• Timeout: {benchmark.profile.recommended_timeout_sec}s\n\n" f"CPU: {benchmark.profile.cpu_cores}C/{benchmark.profile.cpu_threads}T\n" f"RAM: {benchmark.profile.ram_total_gb}GB ({benchmark.profile.ram_available_gb}GB free)" ) header.addWidget(self.hw_badge) logger.debug("[PredictionPanel] Hardware badge OK") except Exception as e: logger.warning(f"[PredictionPanel] Hardware badge failed: {e}") header.addStretch() layout.addLayout(header) logger.debug("[PredictionPanel] Header OK") # 2. TABS (Table / JSON / CSV) self.tabs = QTabWidget() self.tabs.setStyleSheet(""" QTabWidget::pane { border: none; background: #1f2937; } QTabBar::tab { background: #111827; color: #9ca3af; padding: 8px 12px; border: none; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:selected { background: #374151; color: #fff; font-weight: bold; } """) logger.debug("[PredictionPanel] Tabs created") # --- TAB 1: TABLEAU --- self.tab_table = QWidget() l_table = QVBoxLayout(self.tab_table) l_table.setContentsMargins(0, 0, 0, 0) # ACTIONS BAR (Prediction Toolbar) actions_frame = QFrame() actions_frame.setStyleSheet("background: #111827; border-radius: 6px; padding: 4px;") actions_layout = QHBoxLayout(actions_frame) actions_layout.setContentsMargins(5,5,5,5) actions_layout.setSpacing(8) actions_lbl = QLabel("Prediction :") actions_lbl.setStyleSheet("color: #60a5fa; font-weight: bold; font-family: 'Segoe UI'; font-size: 13px;") actions_layout.addWidget(actions_lbl) logger.debug("[PredictionPanel] Loading settings and widgets...") from config.settings import TIMEFRAME_NAMES from gui.widgets.styled_button import Premium3DButton logger.debug("[PredictionPanel] Loading LivePredictionWidget...") from gui.widgets.live_prediction_widget import LivePredictionWidget self.predict_btns = {} self._predict_timers = {} # TIMEFRAME BUTTONS (Flat List) for tf in TIMEFRAME_NAMES: btn = Premium3DButton(tf, color="#4b5563", hover_color="#6b7280", pressed_color="#374151") btn.setCheckable(True) btn.setFixedSize(75, 34) # Callback closure def make_callback(timeframe): def callback(checked): self._on_predict_toggle(timeframe, checked) return callback btn.clicked.connect(make_callback(tf)) self.predict_btns[tf] = btn actions_layout.addWidget(btn) actions_layout.addStretch() l_table.addWidget(actions_frame) logger.debug("[PredictionPanel] Timeframe buttons OK") # --- HUD WIDGET (Replaces Table) --- logger.debug("[PredictionPanel] Creating LivePredictionWidget...") self.live_widget = LivePredictionWidget() l_table.addWidget(self.live_widget) logger.debug("[PredictionPanel] LivePredictionWidget OK") self.tabs.addTab(self.tab_table, "🚀 Live Operations") # --- TAB 2: SIGNAL (RICH HTML) --- self.tab_json = QWidget() l_json = QVBoxLayout(self.tab_json) self.json_viewer = QTextEdit() self.json_viewer.setReadOnly(True) self.json_viewer.setStyleSheet(""" background-color: #0d1117; color: #c9d1d9; font-family: 'Segoe UI', sans-serif; font-size: 13px; border: none; padding: 20px; """) l_json.addWidget(self.json_viewer) self.tabs.addTab(self.tab_json, "🚦 Signal") logger.debug("[PredictionPanel] Signal tab OK") # --- TAB 3: EA MANAGER --- logger.debug("[PredictionPanel] Loading EAManagerWidget...") try: from gui.widgets.ea_manager_widget import EAManagerWidget self.ea_manager = EAManagerWidget(app_controller=self.app_controller) self.tabs.addTab(self.ea_manager, "🤖 EA Manager") logger.debug("[PredictionPanel] EAManagerWidget OK") except Exception as e: logger.error(f"[PredictionPanel] EAManagerWidget FAILED: {e}", exc_info=True) # Create a placeholder tab instead of crashing placeholder = QWidget() placeholder_layout = QVBoxLayout(placeholder) placeholder_layout.addWidget(QLabel(f"⚠️ EA Manager failed to load:\n{e}")) self.tabs.addTab(placeholder, "🤖 EA Manager (Error)") # --- TAB 4: CSV/EXPORT --- self.tab_csv = QWidget() l_csv = QVBoxLayout(self.tab_csv) l_csv.setAlignment(Qt.AlignmentFlag.AlignCenter) btn_csv = QPushButton("📁 Open Data Folder (CSV/JSON)") btn_csv.setFixedSize(300, 50) btn_csv.setStyleSheet(""" QPushButton { background: #2563eb; color: white; border-radius: 6px; font-weight: bold; } QPushButton:hover { background: #1d4ed8; } """) btn_csv.clicked.connect(self._open_data_folder) l_csv.addWidget(btn_csv) lbl_info = QLabel("Predictions are automatically saved as CSV and JSON in 'data/signals'.") lbl_info.setStyleSheet("color: #9ca3af; margin-top: 10px;") l_csv.addWidget(lbl_info) # layout.addWidget(self.tabs) # Previous line was layout.addWidget(self.tabs) layout.addWidget(self.tabs) logger.info("[PredictionPanel] _init_ui COMPLETE") except Exception as e: logger.critical(f"[PredictionPanel] _init_ui CRASHED: {e}", exc_info=True) # Re-raise to show the error, but at least we logged it raise def _refresh_all(self): """Update status with safety""" try: self._update_status() except Exception as e: logger.error(f"PredictionPanel refresh error: {e}") def _update_status(self): # OPTIMIZATION: Skip if not visible if not self.isVisible(): return try: active = False act = "Idle" hb = None if self.app_controller and getattr(self.app_controller, 'scheduler', None): status = self.app_controller.scheduler.get_status() active = status.get("active", False) act = status.get("activity", "Idle") hb = status.get("heartbeat") icon = "🟢" if active else "🔴" ago = "" if hb: delta = int((datetime.now() - hb).total_seconds()) ago = f"({delta}s ago)" # self.status_lbl.setText(f"{icon} {act} {ago}") except Exception: # self.status_lbl.setText("🔴 Disconnected") pass def _open_data_folder(self): from config.settings import SIGNALS_DIR folder = str(SIGNALS_DIR.resolve()) QDesktopServices.openUrl(QUrl.fromLocalFile(folder)) def _on_predict_toggle(self, tf, active): """Handle toggle of prediction button - SINGLE TF MODE with safe switching""" logger.info(f"[TOGGLE] _on_predict_toggle called: tf={tf}, active={active}") if not self.app_controller: logger.warning("[TOGGLE] No app_controller, aborting") return btn = self.predict_btns.get(tf) if active: # === SAFE SWITCH: Deactivate all others first === for other_tf, other_btn in self.predict_btns.items(): if other_tf != tf and other_btn.isChecked(): # Stop the other timeframe self._safe_deactivate(other_tf) other_btn.blockSignals(True) # Prevent recursive signal other_btn.setChecked(False) other_btn.blockSignals(False) # === ACTIVATE NEW TF === if btn: btn.set_color("#059669", "#10b981", "#047857") # Green btn.setText(f"✓ {tf}") self._active_tf = tf # Track active TF self._is_prediction_running = False self._add_log(f"🚀 Starting Instant Prediction for {tf}...") # Reset HUD for new TF self.live_widget.reset(tf) # Start IMMEDIATELY (User feedback is priority) self._execute_scheduled_prediction(tf) else: # === DEACTIVATE === self._safe_deactivate(tf) def _safe_deactivate(self, tf): """Safely deactivate a timeframe with proper cleanup""" btn = self.predict_btns.get(tf) # Reset button style if btn: btn.set_color("#4b5563", "#6b7280", "#374151") # Default Grey btn.setText(f"{tf}") # Cancel prediction timer if tf in self._predict_timers: self._predict_timers[tf].stop() del self._predict_timers[tf] # Cancel countdown timer if hasattr(self, '_countdown_timer') and self._countdown_timer.isActive(): self._countdown_timer.stop() # Clear active TF tracking if hasattr(self, '_active_tf') and self._active_tf == tf: self._active_tf = None # Clear Main Window Display if this was the active one self.prediction_updated.emit(tf, "", 0.0) self._add_log(f"⏹ [{tf}] Prediction stopped safely") def _schedule_next_55s(self, tf): """Schedule execution for the next VALID 55s mark based on timeframe""" now = datetime.now() # Parse Interval interval = 1 if tf == "5m": interval = 5 elif tf == "15m": interval = 15 elif tf == "30m": interval = 30 elif tf == "1h": interval = 60 elif tf == "4h": interval = 240 elif tf == "1d": interval = 1440 elif tf == "1w": interval = 10080 # Calculate target aligned to timeframe close target = now.replace(second=55, microsecond=0) # If target has already passed in this minute, move to next minute first if target <= now: target += timedelta(minutes=1) # Align to interval # optimization: use math instead of loop if interval > 1: # Current minute of target minute = target.minute remainder = minute % interval if remainder != 0: # Add enough minutes to reach next multiple of interval minutes_to_add = interval - remainder # Special case: if we wrap around hour/day, timedelta handles it target += timedelta(minutes=minutes_to_add) # Safety: Ensure absolute future if target <= now: target += timedelta(minutes=interval) logger.info(f"[{tf}] Scheduled target: {target} (Now: {now})") delta_ms = int((target - now).total_seconds() * 1000) # Visual feedback wait_sec = delta_ms / 1000 msg = f"⏳ Syncing {tf} to {target.strftime('%H:%M:%S')} ({wait_sec:.0f}s)" # self.status_lbl.setText(msg) # REMOVED # self.status_lbl.setStyleSheet("color: #60a5fa;") # Start Countdown Timer self._target_time = target self._wait_start_time = datetime.now() # Track start for Count UP self._countdown_timer = QTimer() self._countdown_timer.timeout.connect(lambda: self._update_countdown(tf)) self._countdown_timer.start(1000) # Update HUD Sync Status immediately (0 elapsed) self.live_widget.set_sync_status(0) # Schedule timer = QTimer() timer.setSingleShot(True) timer.timeout.connect(lambda: self._execute_scheduled_prediction(tf)) timer.start(delta_ms) self._predict_timers[tf] = timer def _update_countdown(self, tf): """Update the countdown display (Count UP)""" if not hasattr(self, '_wait_start_time'): return now = datetime.now() # Calculate elapsed seconds since start of wait elapsed = (now - self._wait_start_time).total_seconds() # Cap at total wait duration if needed, but for visual we just show elapsed # Or do we want 0..60 specifically? # If the wait is 35s, it will go 0..35. # If the user wants 0..60 regardless of actual wait, that's fake. # Assuming he wants elapsed time. self.live_widget.set_sync_status(elapsed) if now >= self._target_time: self._countdown_timer.stop() def _execute_scheduled_prediction(self, tf): """Execute prediction and schedule next loop - WITH DETAILED STEP TRACKING""" logger.info(f"[{tf}] _execute_scheduled_prediction START") try: # Double check if still active btn = self.predict_btns.get(tf) if not btn or not btn.isChecked(): logger.warning(f"[{tf}] Button not checked, aborting") return # STOP COUNTDOWN TIMER to prevent UI race condition if hasattr(self, '_countdown_timer') and self._countdown_timer.isActive(): self._countdown_timer.stop() logger.debug(f"[{tf}] Updating HUD...") # Update HUD: Sync Done, Start Pipeline self.live_widget.number_lbl.setText("...") self.live_widget.sub_lbl.setText("PROCESSING") self.live_widget.sub_lbl.setStyleSheet("color: #60a5fa; background: transparent; letter-spacing: 1px;") self.live_widget.update_status("sync", "done", elapsed_sec=0) self.live_widget.update_status("hardware", "active") logger.debug(f"[{tf}] HUD updated, now importing QThread...") # === RUN IN SEPARATE THREAD (QThread + Worker) === from PyQt6.QtCore import QThread logger.debug(f"[{tf}] QThread imported, now importing PredictionWorker...") try: from gui.workers.prediction_worker import PredictionWorker logger.debug(f"[{tf}] PredictionWorker imported successfully") except Exception as import_err: logger.critical(f"[{tf}] FAILED to import PredictionWorker: {import_err}", exc_info=True) raise # Create Thread and Worker logger.debug(f"[{tf}] Creating QThread and Worker...") thread = QThread() # Use local variable self.thread = thread # Update member for external access if needed (but safer to rely on list) worker = PredictionWorker(self.app_controller, tf) worker.moveToThread(thread) logger.debug(f"[{tf}] Worker created and moved to thread") # Connect Signals thread.started.connect(worker.run) worker.finished.connect(thread.quit) worker.finished.connect(worker.deleteLater) thread.finished.connect(thread.deleteLater) # Custom Handlers worker.step_updated.connect(lambda step, state, elapsed: self.live_widget.update_status(step, state, elapsed_sec=elapsed)) worker.error.connect(lambda step, msg: self._handle_prediction_error(tf, step, msg)) worker.error.connect(thread.quit) worker.error.connect(worker.deleteLater) # Thread cleanup safety if not hasattr(self, '_active_threads'): self._active_threads = [] # Closure capturing local 'thread' variable correctly def cleanup_thread(): if thread in self._active_threads: self._active_threads.remove(thread) # thread.deleteLater() # Already connected above thread.finished.connect(cleanup_thread) self._active_threads.append(thread) def on_cycle_finished(result): # 1. Update UI with result self._on_prediction_done(tf, result) # 2. Start Red Wait Cycle (Main Thread Timers) logger.info(f"[{tf}] Starting 5s wait count-up from Thread return...") # We schedule the visual countdown 0..4 for elapsed in range(0, 5): QTimer.singleShot(elapsed * 1000, lambda e=elapsed: self.live_widget.set_wait_status(e)) # 3. Reschedule after 5s # Note: We trigger reschedule slightly after the 5th second visual update QTimer.singleShot(5000, lambda: self.live_widget.set_wait_status(5)) QTimer.singleShot(5100, lambda: self._reschedule_loop(tf)) worker.finished.connect(on_cycle_finished) # Start logger.info(f"[{tf}] Starting prediction thread...") thread.start() # Keep reference to avoid GC (Current active one) self._current_worker = worker # self._current_thread handled by _active_threads list now logger.info(f"[{tf}] _execute_scheduled_prediction COMPLETE - thread running") except Exception as e: logger.critical(f"[{tf}] _execute_scheduled_prediction CRASHED: {e}", exc_info=True) raise return def _handle_prediction_error(self, tf, phase, error_msg): """Handle prediction error and update UI accordingly""" self._add_log(f"❌ [{tf}] Error in {phase}: {error_msg[:50]}...") # Update HUD to show error state if phase == "data": self.live_widget.update_status("data", "error") elif phase == "ai": self.live_widget.update_status("data", "done") self.live_widget.update_status("ai", "error") elif phase == "risk": self.live_widget.update_status("data", "done") self.live_widget.update_status("ai", "done") self.live_widget.update_status("risk", "error") # Show error in result area self.live_widget.show_error(error_msg[:60]) # Enrich error message with solution suggestion from core.error_solutions import enrich_error enriched_msg = enrich_error(error_msg) # Update JSON viewer with solution hint self.json_viewer.setText(f"❌ ERROR [{tf}]\n\nPhase: {phase.upper()}\n\n{enriched_msg}") # Wait 5s then reschedule (don't stop, keep trying) QTimer.singleShot(5000, lambda: self._reschedule_loop(tf)) def _handle_prediction_warning(self, tf, warning_msg): """Handle recoverable warning - continue loop""" self._add_log(f"⚠️ [{tf}] Warning: {warning_msg[:50]}...") # Update HUD with warning state self.live_widget.update_status("data", "warning") self.live_widget.update_status("ai", "done") self.live_widget.update_status("risk", "warning") self.live_widget.update_status("result", "warning") # Show warning in result self.live_widget.show_warning(warning_msg[:40]) # Continue with next cycle QTimer.singleShot(5000, lambda: self._reschedule_loop(tf)) def _handle_critical_error(self, tf, error_msg): """Stop prediction and inform user of critical failure""" self._add_log(f"❌ STOPPING {tf}: {error_msg}") # self.status_lbl.setStyleSheet("color: #ef4444;") # REMOVED # Stop Auto-Prediction Toggle if tf in self.predict_btns: self.predict_btns[tf].setChecked(False) self._on_predict_toggle(tf, False) # Logic to stop timers # Update HUD to Fault State self.live_widget.reset(tf) # Or specific error state self.live_widget.tf_label.setText(f"ERROR {tf}") self.live_widget.tf_label.setStyleSheet("color: #ef4444; letter-spacing: 2px;") self.live_widget.sub_lbl.setText("STOPPED") # Show specific message in JSON viewer or Banner self.json_viewer.setText(f"🛑 CRITICAL FAILURE:\n{error_msg}\n\nThe process has been stopped.") # Enrich error message with solution suggestion from core.error_solutions import enrich_error enriched_msg = enrich_error(error_msg) from PyQt6.QtWidgets import QMessageBox QMessageBox.critical(self, "Prediction Error", f"Process Stopped for {tf}.\n\n{enriched_msg}") def _reschedule_loop(self, tf): """Reschedule for next minute after the short wait""" logger.info(f"[RESCHEDULE] _reschedule_loop called for {tf}") # Check if still active btn = self.predict_btns.get(tf) if btn and btn.isChecked(): logger.info(f"[RESCHEDULE] Button still active, scheduling next cycle for {tf}") # Reset HUD for next cycle self.live_widget.reset(tf) self._schedule_next_55s(tf) else: logger.info(f"[RESCHEDULE] Button no longer active for {tf}, stopping loop") def _add_log(self, message): # Helper to log to status or print logger.info(message) # self.status_lbl.setText(message) # REMOVED def _on_prediction_done(self, tf, result): """Handle prediction result""" if "error" in result: # self.status_lbl.setText(f"❌ Error {tf}: {result['error']}") # REMOVED pass else: # self.status_lbl.setText(f"✅ Prediction {tf} Done") # REMOVED pass # Update HUD Steps data_status = result.get("data_status", "fresh") data_age = result.get("data_age", "") if "stale" in data_status: self.live_widget.update_status("data", "warning", f"old {data_age}") else: self.live_widget.update_status("data", "done") self.live_widget.update_status("ai", "done") # Instant in this flow self.live_widget.update_status("risk", "done") # Implicit self.live_widget.update_status("result", "done") # Show Result Banner sig = result.get("signal", "NEUTRAL") conf = result.get("confidence", 0.0) score = result.get("score", 0.0) trend_certainty = result.get("trend_certainty", 0.0) duration_tf5 = result.get("duration_tf5", 0) # Extract override from congress decision override_type = None if result.get("congress_decision"): override_type = result["congress_decision"].get("override_type") self.live_widget.show_result(sig, conf, score, override_type, trend_certainty, duration_tf5) # Update Main Window Toolbar self.prediction_updated.emit(tf, sig, conf) # Direct UI Update (Memory Based) self._update_signal_display(tf, result) def _update_signal_display(self, tf, result_data=None): """Populate Signal tab with Premium Trading Terminal UI""" from config.settings import SIGNALS_DIR from pathlib import Path import json # 1. Resolve Data Source (Memory > File) data = result_data path = None # If no memory data, try loading latest file if not data: path = SIGNALS_DIR / f"EURUSD_{tf}_latest.json" if path.exists(): try: with open(path) as f: data = json.load(f) except: data = None # 2. Extract Path for Footer if data and "file_path" in data: path_str = data["file_path"] elif path: path_str = str(path.absolute()) else: path_str = "No file generated yet" # 3. Build Premium UI html_content = "" if data: # Extract Main Values signal = data.get("signal") or "NEUTRAL" confidence = data.get("confidence", 0.0) score = data.get("score", 0.0) regime = data.get("regime", "Unknown").replace('_', ' ').title() override_type = data.get("override_type") or data.get("congress_decision", {}).get("override_type") timestamp = self._format_timestamp(data.get('timestamp', '--')) # Model breakdown models = data.get("model_signals", {}) # Styles & Icons based on signal if signal == "BUY": gradient = "linear-gradient(135deg, #064e3b 0%, #065f46 50%, #047857 100%)" signal_color = "#10b981" icon = "▲" arrow_anim = "pulse-up" bar_color = "#10b981" elif signal == "SELL": gradient = "linear-gradient(135deg, #7f1d1d 0%, #991b1b 50%, #b91c1c 100%)" signal_color = "#ef4444" icon = "▼" arrow_anim = "pulse-down" bar_color = "#ef4444" else: gradient = "linear-gradient(135deg, #1f2937 0%, #374151 50%, #4b5563 100%)" signal_color = "#9ca3af" icon = "◆" arrow_anim = "" bar_color = "#6b7280" # Override badge override_html = "" if override_type: override_clean = override_type.replace('_', ' ').upper() override_html = f''' <div style="position: absolute; top: 15px; right: 15px; background: #f59e0b; color: #000; font-weight: 800; font-size: 9px; padding: 4px 10px; border-radius: 4px; letter-spacing: 1px; box-shadow: 0 2px 8px rgba(245,158,11,0.4);"> {override_clean} </div>''' # Confidence bar width conf_pct = int(confidence * 100) # Model signals HTML model_html = "" for model_name, model_data in models.items(): m_signal = model_data.get("signal", "?") m_conf = model_data.get("confidence", 0) m_color = "#10b981" if m_signal == "BUY" else "#ef4444" if m_signal == "SELL" else "#6b7280" # Smart Display for 0% (Mismatch/Untrained State) if m_conf == 0 and m_signal == "NEUTRAL": conf_display = '<span style="color: #f59e0b; font-size: 10px;">⚠️ RETRAIN</span>' else: conf_display = f'<span style="color: #6b7280;">({m_conf:.0%})</span>' model_html += f''' <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #111827; border-radius: 6px; margin-bottom: 6px;"> <span style="color: #9ca3af; font-size: 11px; text-transform: uppercase; font-weight: 600;">{model_name}</span> <span style="color: {m_color}; font-weight: 700; font-size: 12px;">{m_signal} {conf_display}</span> </div>''' # Build Premium HTML html_content = f''' <style> @keyframes pulse-up {{ 0%, 100% {{ transform: translateY(0); opacity: 1; }} 50% {{ transform: translateY(-8px); opacity: 0.7; }} }} @keyframes pulse-down {{ 0%, 100% {{ transform: translateY(0); opacity: 1; }} 50% {{ transform: translateY(8px); opacity: 0.7; }} }} @keyframes glow {{ 0%, 100% {{ box-shadow: 0 0 20px {signal_color}33; }} 50% {{ box-shadow: 0 0 40px {signal_color}66; }} }} </style> <div style="font-family: 'Segoe UI', -apple-system, sans-serif; color: #e5e7eb; padding: 20px; background: #0d1117;"> <!-- HERO SIGNAL CARD --> <div style="position: relative; background: {gradient}; border-radius: 16px; padding: 40px 30px; text-align: center; margin-bottom: 20px; border: 1px solid {signal_color}44; box-shadow: 0 8px 32px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.1); animation: glow 2s ease-in-out infinite;"> {override_html} <!-- Arrow Icon --> <div style="font-size: 56px; color: {signal_color}; margin-bottom: 5px; text-shadow: 0 0 30px {signal_color}; animation: {arrow_anim} 1.5s ease-in-out infinite;"> {icon} </div> <!-- Signal Text --> <div style="font-size: 42px; font-weight: 900; color: white; letter-spacing: 4px; text-shadow: 0 2px 10px rgba(0,0,0,0.5);"> {signal} </div> <!-- Confidence Bar --> <div style="margin-top: 20px; background: rgba(0,0,0,0.3); border-radius: 10px; height: 12px; overflow: hidden; max-width: 280px; margin-left: auto; margin-right: auto;"> <div style="width: {conf_pct}%; height: 100%; background: {bar_color}; border-radius: 10px; transition: width 0.5s ease;"></div> </div> <div style="font-size: 14px; color: rgba(255,255,255,0.8); margin-top: 8px; font-weight: 600;"> CONFIDENCE: <span style="color: white; font-size: 18px;">{confidence:.1%}</span> </div> </div> <!-- METRICS ROW --> <div style="display: flex; gap: 12px; margin-bottom: 20px;"> <div style="flex: 1; background: linear-gradient(180deg, #1f2937 0%, #111827 100%); padding: 18px; border-radius: 12px; text-align: center; border: 1px solid #374151;"> <div style="font-size: 10px; color: #6b7280; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px;">Timeframe</div> <div style="font-size: 20px; font-weight: 700; color: #60a5fa;">{tf}</div> </div> <div style="flex: 1; background: linear-gradient(180deg, #1f2937 0%, #111827 100%); padding: 18px; border-radius: 12px; text-align: center; border: 1px solid #374151;"> <div style="font-size: 10px; color: #6b7280; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px;">Score</div> <div style="font-size: 20px; font-weight: 700; color: {signal_color};">{score:+.3f}</div> </div> <div style="flex: 1; background: linear-gradient(180deg, #1f2937 0%, #111827 100%); padding: 18px; border-radius: 12px; text-align: center; border: 1px solid #374151;"> <div style="font-size: 10px; color: #6b7280; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px;">Regime</div> <div style="font-size: 14px; font-weight: 600; color: #d1d5db;">{regime}</div> </div> </div> <!-- MODEL BREAKDOWN --> <div style="background: #1f2937; border-radius: 12px; padding: 15px; margin-bottom: 20px; border: 1px solid #374151;"> <div style="font-size: 11px; color: #6b7280; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; font-weight: 600;"> 🤖 Model Breakdown </div> {model_html if model_html else '<div style="color: #4b5563; font-size: 12px; text-align: center;">No model data available</div>'} </div> <!-- TIMESTAMP --> <div style="text-align: center; margin-bottom: 20px;"> <div style="font-size: 10px; color: #4b5563; text-transform: uppercase; letter-spacing: 1px;">Generated</div> <div style="font-size: 13px; color: #9ca3af; font-family: 'Consolas', monospace;">{timestamp}</div> </div> <!-- FOOTER --> <div style="border-top: 1px solid #374151; padding-top: 12px; font-size: 9px; color: #374151; text-align: center; word-break: break-all;"> 📂 {path_str} </div> </div> ''' else: # Idle State - Premium Empty State html_content = f''' <div style="font-family: 'Segoe UI', sans-serif; text-align: center; color: #4b5563; padding: 60px 20px; background: #0d1117;"> <div style="font-size: 64px; margin-bottom: 20px; opacity: 0.3;">📊</div> <h2 style="color: #6b7280; font-weight: 600; margin-bottom: 10px;">No Signal for {tf}</h2> <p style="color: #4b5563; font-size: 13px;">Activate prediction to generate a live trading signal.</p> <div style="margin-top: 30px; padding: 15px 25px; background: #1f2937; border-radius: 8px; display: inline-block; border: 1px solid #374151;"> <span style="color: #60a5fa;">💡 Tip:</span> <span style="color: #9ca3af;">Click a timeframe button above to start</span> </div> </div> ''' self.json_viewer.setHtml(html_content) def _format_timestamp(self, ts_str): """Format timestamp to readable string""" if not ts_str or ts_str == '--': return '--' try: # Handle ISO format dt = datetime.fromisoformat(str(ts_str).replace('Z', '+00:00')) return dt.strftime('%Y-%m-%d %H:%M:%S') except Exception: # Fallback return str(ts_str)