"""
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)