Source code for gui.panels.dashboard_panel

"""
Cerebrum Forex - Dashboard Panel
Main dashboard with tabbed layout: Signals & Chart, Models & Timeframes
"""

import logging
from datetime import datetime

from PyQt6.QtWidgets import (
    QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame,
    QGridLayout, QComboBox, QGroupBox, QPushButton, QDialog,
    QSizePolicy, QTabWidget, QLineEdit, QCheckBox, QProgressBar
)
from datetime import datetime, timedelta

from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal
from PyQt6.QtGui import QFont, QColor

from gui.panels.dashboard_widgets.chart_widget import CandlestickWidget
from gui.widgets.styled_button import Premium3DButton
from core.noble_kpi import analyze_timeframe
from config.settings import OHLC_DIR
import pandas as pd

[docs] class PredictionWorker(QThread): """Background worker for Full AI Prediction (Congress Engine)""" finished = pyqtSignal(dict, str) # result, tf error = pyqtSignal(str) def __init__(self, app_controller, tf): super().__init__() self.app_controller = app_controller self.tf = tf
[docs] def run(self): try: if not self.app_controller or not self.app_controller.prediction_engine: self.error.emit("Engine Not Ready") return # Use the full Congress Engine Prediction # This calculates Noble KPI internally or we can extract it # But predict_timeframe returns the final signal which includes Noble Logic result = self.app_controller.prediction_engine.predict_timeframe(self.tf) if not result: self.error.emit("Prediction Failed") return # Also get pure Noble KPI for the gauge display # We can calculate it quickly here to ensure we have the specific gauge values # (predict_timeframe might not return safe low/high explicitly in its top level dict) from config.settings import OHLC_DIR ohlc_file = OHLC_DIR / f"ohlc_EURUSD_{self.tf}.csv" if ohlc_file.exists(): df = pd.read_csv(ohlc_file) if not df.empty: noble_result = analyze_timeframe(df, self.tf) # Rename Noble keys to avoid overwriting AI Congress Decision if 'signal' in noble_result: noble_result['kpi_signal'] = noble_result.pop('signal') if 'description' in noble_result: noble_result['kpi_desc'] = noble_result.pop('description') if 'confidence' in noble_result: noble_result['kpi_conf'] = noble_result.pop('confidence') # Merge noble results into prediction result (Safe Merge) result.update(noble_result) self.finished.emit(result, self.tf) except Exception as e: logger.error(f"Prediction worker error: {e}", exc_info=True) self.error.emit(str(e))
import pandas as pd from core.noble_kpi import calculate_atr, K_CONSTANTS from pathlib import Path from core.profiler import profile logger = logging.getLogger(__name__) # Imported Widgets from gui.panels.dashboard_widgets.chart_widget import CandlestickWidget, FullscreenChartDialog from gui.panels.dashboard_widgets.safe_range_widget import SafeRangeWidget
[docs] class DashboardPanel(QWidget): """Main dashboard panel with tabs""" def __init__(self, app_controller=None): super().__init__() logger.info("DashboardPanel: Initializing...") self.app_controller = app_controller try: self._init_ui() logger.info("DashboardPanel: UI Initialized Successfully") except Exception as e: logger.error(f"DashboardPanel: UI Initialization Failed! Error: {e}", exc_info=True) # Create a fallback label so we see something layout = QVBoxLayout(self) label = QLabel(f"Dashboard Error: {e}") label.setStyleSheet("color: red; font-size: 16px;") layout.addWidget(label) self._timer = QTimer() self._timer.timeout.connect(self._update_dashboard) self._timer.start(5000) # Reduced frequency to avoid blocking # State for background chart updates self._chart_updating = False self._last_live_update_time = 0 # Timestamp of last live update self._LIVE_UPDATE_INTERVAL_SEC = 30 # Only fetch new data every 30s # === STAGGERED INITIALIZATION (Non-Blocking) === # 1.5s: Main Chart (Medium) QTimer.singleShot(1500, lambda: self._on_tf_button_click("4h")) # 2.0s: Safe Range (Heavy) QTimer.singleShot(2000, self._refresh_safe_range) def _init_ui(self): # Main Layout layout = QVBoxLayout(self) layout.setSpacing(10) layout.setContentsMargins(10, 10, 10, 10) layout.setAlignment(Qt.AlignmentFlag.AlignTop) # Header header = QLabel("📊 Dashboard") header.setFont(QFont("Segoe UI", 18, QFont.Weight.Bold)) header.setStyleSheet("color: #0e639c;") layout.addWidget(header) # Tabs self.tabs = QTabWidget() self.tabs.setStyleSheet(""" QTabWidget::pane { border: none; background-color: #252526; border-radius: 5px; } QTabBar::tab { background-color: #2d2d2d; color: #cccccc; padding: 10px 25px; border: none; border-bottom: none; border-top-left-radius: 5px; border-top-right-radius: 5px; } QTabBar::tab:selected { background-color: #252526; border-top: 2px solid #0e639c; } QTabBar::tab:hover { background-color: #383838; } QLabel { border: none; } """) # Tab 1: Signals & Chart self.signals_tab = self._create_signals_tab() self.tabs.addTab(self.signals_tab, "📈 Signals - Chart") # Tab 2: Trading Operations (Single View) self.noble_tab = self._create_models_tab() self.tabs.addTab(self.noble_tab, "📊 Trading") # Tab 3: Economic Calendar self.calendar_tab = self._create_calendar_tab() self.tabs.addTab(self.calendar_tab, "📅 Calendar") # Tab 4: MT5 Account Info self.mt5_info_tab = self._create_mt5_info_tab() self.tabs.addTab(self.mt5_info_tab, "💰 MT5") # Note: Auto-Train moved to Training Panel popup (User Request) layout.addWidget(self.tabs) # Status bar status_row = QHBoxLayout() self.system_status = QLabel("● System OK") self.system_status.setFont(QFont("Segoe UI", 11)) self.system_status.setStyleSheet("color: #10b981;") status_row.addWidget(self.system_status) status_row.addStretch() # Market status indicator (center) self.market_status = QLabel("🔄 Checking...") self.market_status.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold)) self.market_status.setStyleSheet("color: #60a5fa;") self.market_status.setAlignment(Qt.AlignmentFlag.AlignCenter) status_row.addWidget(self.market_status) # Start market status timer self._market_timer = QTimer() self._market_timer.timeout.connect(self._update_market_status) self._market_timer.start(1000) # Update every second QTimer.singleShot(500, self._update_market_status) # Initial update status_row.addStretch() self.last_update = QLabel("Last update: --") self.last_update.setStyleSheet("color: #6b7280;") status_row.addWidget(self.last_update) layout.addLayout(status_row) def _create_signals_tab(self): """Create Signals & Chart tab""" tab = QWidget() layout = QVBoxLayout(tab) layout.setSpacing(10) layout.setContentsMargins(10, 10, 10, 10) # Chart: Chart takes full space now chart_frame = QFrame() chart_frame.setStyleSheet(""" QFrame { background-color: #1e1e1e; border: none; border-radius: 8px; } """) chart_layout = QVBoxLayout(chart_frame) chart_layout.setContentsMargins(10, 10, 10, 10) layout.addWidget(chart_frame) # Chart header chart_header = QHBoxLayout() chart_title = QLabel("📈 EUR/USD Chart") chart_title.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold)) chart_title.setStyleSheet("color: #0e639c;") chart_header.addWidget(chart_title) chart_header.addStretch() # Status label (centered in header) self.chart_status = QLabel("Click a timeframe") self.chart_status.setStyleSheet("color: #10b981; font-size: 11px;") self.chart_status.setAlignment(Qt.AlignmentFlag.AlignCenter) chart_header.addWidget(self.chart_status) chart_header.addStretch() fullscreen_btn = Premium3DButton("⛶ Fullscreen", color="#3c3c3c", hover_color="#505050", pressed_color="#2d2d2d") fullscreen_btn.clicked.connect(self._open_fullscreen_chart) chart_header.addWidget(fullscreen_btn) chart_layout.addLayout(chart_header) # Timeframe toolbar (clickable buttons) tf_toolbar = QHBoxLayout() tf_toolbar.setSpacing(5) self.tf_buttons = {} timeframes = ["1m", "5m", "15m", "30m", "1h", "4h"] tf_toolbar.addStretch() # Center alignment start for tf in timeframes: btn = Premium3DButton(tf, color="#2d2d2d", hover_color="#3c3c3c", pressed_color="#505050") btn.setFixedWidth(60) btn.setFont(QFont("Segoe UI", 10, QFont.Weight.Bold)) # Increase font slightly and bold for readability btn.clicked.connect(lambda checked, t=tf: self._on_tf_button_click(t)) self.tf_buttons[tf] = btn tf_toolbar.addWidget(btn) # Set 4h as default selected self._selected_tf = "4h" self.tf_buttons["4h"].set_color("#0e639c", "#0e639c", "#0c4a75") # Cooldown indicator label self.tf_cooldown_label = QLabel("") self.tf_cooldown_label.setStyleSheet("color: #60a5fa; font-size: 14px;") tf_toolbar.addWidget(self.tf_cooldown_label) tf_toolbar.addStretch() chart_layout.addLayout(tf_toolbar) # Candlestick chart widget self.chart_widget = CandlestickWidget() self.chart_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) chart_layout.addWidget(self.chart_widget) return tab def _on_tf_button_click(self, tf: str): """Handle timeframe button click with cooldown""" # Check cooldown - prevent rapid clicks if hasattr(self, '_tf_cooldown') and self._tf_cooldown: return # Ignore click during cooldown # Set cooldown self._tf_cooldown = True # Disable all buttons during loading for btn in self.tf_buttons.values(): btn.setEnabled(False) # Update button styles for t, btn in self.tf_buttons.items(): if t == tf: # Active: Blue btn.set_color("#0e639c", "#0e639c", "#0c4a75") else: # Inactive: Dark btn.set_color("#2d2d2d", "#3c3c3c", "#505050") self._selected_tf = tf self.chart_status.setText("⏳ Loading...") self.chart_status.setStyleSheet("color: #60a5fa;") # Show loading overlay in center of chart self.tf_cooldown_label.setText("") self._show_loading_overlay() # Load chart (will release cooldown when done) QTimer.singleShot(50, lambda: self._do_refresh_chart(tf)) def _release_tf_cooldown(self): """Release the TF button cooldown""" self._tf_cooldown = False self.tf_cooldown_label.setText("") for btn in self.tf_buttons.values(): btn.setEnabled(True) # Hide loading overlay if hasattr(self, 'loading_overlay') and self.loading_overlay: self.loading_overlay.hide() def _show_loading_overlay(self): """Show loading overlay in center of chart""" if not hasattr(self, 'loading_overlay') or not self.loading_overlay: self.loading_overlay = QLabel("⏳ Loading...", self.chart_widget) self.loading_overlay.setStyleSheet(""" background-color: rgba(0, 0, 0, 0.8); color: #60a5fa; font-size: 18px; font-weight: bold; padding: 20px 40px; border-radius: 10px; """) self.loading_overlay.setAlignment(Qt.AlignmentFlag.AlignCenter) # Center the overlay self.loading_overlay.adjustSize() x = (self.chart_widget.width() - self.loading_overlay.width()) // 2 y = (self.chart_widget.height() - self.loading_overlay.height()) // 2 self.loading_overlay.move(max(0, x), max(0, y)) self.loading_overlay.raise_() self.loading_overlay.show() def _refresh_chart(self): """Refresh using current selected TF""" tf = getattr(self, '_selected_tf', '4h') self._on_tf_button_click(tf) def _do_refresh_chart(self, tf): """Actual chart refresh - releases cooldown when done""" try: from config.settings import OHLC_DIR ohlc_file = OHLC_DIR / f"ohlc_EURUSD_{tf}.csv" if ohlc_file.exists(): df = pd.read_csv(ohlc_file) if 'time' in df.columns: df['time'] = pd.to_datetime(df['time'], errors='coerce') df = df.dropna(subset=['time']) if hasattr(self, 'chart_widget') and len(df) > 0: self.chart_widget.update_chart(df, tf) self.chart_status.setText(f"✓ {tf} ({len(df)} candles)") self.chart_status.setStyleSheet("color: #10b981;") else: self.chart_status.setText("❌ Data not found") self.chart_status.setStyleSheet("color: #ef4444;") except pd.errors.ParserError: logger.error(f"Corrupt CSV found: {ohlc_file}. Deleting...") try: ohlc_file.unlink() # Delete corrupt file self.chart_status.setText("⚠️ Corrupt data (re-click)") except: self.chart_status.setText("❌ File error") self.chart_status.setStyleSheet("color: #f59e0b;") except Exception as e: logger.error(f"Chart refresh error: {e}") self.chart_status.setText(f"❌ {str(e)[:30]}") self.chart_status.setStyleSheet("color: #ef4444;") finally: # Release cooldown when loading is actually done self._release_tf_cooldown() def _create_models_tab(self): """Create Safe Range Tab - SMART SINGLE TIMEFRAME VIEW""" tab = QWidget() layout = QVBoxLayout(tab) layout.setSpacing(10) layout.setContentsMargins(15, 15, 15, 15) # ═══════════════════════════════════════════════════════ # HEADER: Timeframe Buttons Toolbar # ═══════════════════════════════════════════════════════ header = QHBoxLayout() # Timeframe button toolbar (replaces combobox + refresh) self.sr_tf_buttons = {} sr_timeframes = ["1m", "5m", "15m", "30m", "1h", "4h"] self._sr_selected_tf = "5m" # Default for tf in sr_timeframes: btn = Premium3DButton(tf, color="#2d2d2d", hover_color="#3c3c3c", pressed_color="#505050") btn.setFixedWidth(65) btn.setFixedHeight(32) btn.setFont(QFont("Segoe UI", 11, QFont.Weight.Bold)) btn.clicked.connect(lambda checked, t=tf: self._on_sr_tf_click(t)) self.sr_tf_buttons[tf] = btn header.addWidget(btn) # Set default button as active self.sr_tf_buttons["5m"].set_color("#059669", "#047857", "#065f46") # Status self.sr_status = QLabel("Select a timeframe") self.sr_status.setStyleSheet("color: #f59e0b; font-size: 11px;") header.addWidget(self.sr_status) header.addStretch() layout.addLayout(header) # ═══════════════════════════════════════════════════════ # MAIN DISPLAY AREA # ═══════════════════════════════════════════════════════ display_frame = QFrame() display_frame.setStyleSheet("background-color: #0f172a; border-radius: 12px; border: none;") display_layout = QVBoxLayout(display_frame) display_layout.setSpacing(12) display_layout.setContentsMargins(20, 20, 20, 20) # Row 1: Current Price (BIG) self.sr_price_label = QLabel("--") self.sr_price_label.setFont(QFont("Segoe UI", 36, QFont.Weight.Bold)) self.sr_price_label.setStyleSheet("color: #ffffff;") self.sr_price_label.setAlignment(Qt.AlignmentFlag.AlignCenter) display_layout.addWidget(self.sr_price_label) # Row 2: Position indicator self.sr_position_label = QLabel("Position: --%") self.sr_position_label.setFont(QFont("Segoe UI", 14)) self.sr_position_label.setStyleSheet("color: #9ca3af;") self.sr_position_label.setAlignment(Qt.AlignmentFlag.AlignCenter) display_layout.addWidget(self.sr_position_label) # === VISUAL SAFE RANGE GAUGE === gauge_frame = QFrame() gauge_frame.setStyleSheet(""" QFrame { background-color: #1a1a1b; border-radius: 10px; border: none; } """) gauge_frame.setMinimumHeight(110) gauge_layout = QVBoxLayout(gauge_frame) gauge_layout.setSpacing(8) gauge_layout.setContentsMargins(15, 10, 15, 10) # Labels row: SELL ZONE | NEUTRAL | BUY ZONE zone_labels = QHBoxLayout() sell_zone = QLabel("🔴 SELL ZONE") sell_zone.setStyleSheet("color: #ef4444; font-size: 11px; font-weight: bold;") zone_labels.addWidget(sell_zone) zone_labels.addStretch() neutral_zone = QLabel("⚪ NEUTRAL") neutral_zone.setStyleSheet("color: #9ca3af; font-size: 11px; background: transparent;") zone_labels.addWidget(neutral_zone) zone_labels.addStretch() buy_zone = QLabel("🟢 BUY ZONE") buy_zone.setStyleSheet("color: #10b981; font-size: 11px; font-weight: bold;") buy_zone.setAlignment(Qt.AlignmentFlag.AlignRight) zone_labels.addWidget(buy_zone) gauge_layout.addLayout(zone_labels) # Progress bar with professional gradient self.sr_gauge = QProgressBar() self.sr_gauge.setRange(0, 100) self.sr_gauge.setValue(50) self.sr_gauge.setTextVisible(False) self.sr_gauge.setFixedHeight(22) self.sr_gauge.setStyleSheet(""" QProgressBar { background-color: #1f2937; border-radius: 11px; border: 1px solid #374151; } QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #ef4444, stop:0.2 #f97316, stop:0.4 #eab308, stop:0.6 #84cc16, stop:0.8 #22c55e, stop:1.0 #10b981); border-radius: 10px; } """) gauge_layout.addWidget(self.sr_gauge) # Price level markers: Safe Low | Current | Safe High markers_row = QHBoxLayout() markers_row.setContentsMargins(5, 2, 5, 0) self.sr_gauge_low = QLabel("--") self.sr_gauge_low.setStyleSheet("color: #ef4444; font-size: 10px; font-weight: bold; background: transparent;") markers_row.addWidget(self.sr_gauge_low) markers_row.addStretch() self.sr_gauge_current = QLabel("--") self.sr_gauge_current.setStyleSheet("color: #ffffff; font-size: 12px; font-weight: 800; background: transparent;") self.sr_gauge_current.setAlignment(Qt.AlignmentFlag.AlignCenter) markers_row.addWidget(self.sr_gauge_current) markers_row.addStretch() self.sr_gauge_high = QLabel("--") self.sr_gauge_high.setStyleSheet("color: #10b981; font-size: 10px; font-weight: bold; background: transparent;") self.sr_gauge_high.setAlignment(Qt.AlignmentFlag.AlignRight) markers_row.addWidget(self.sr_gauge_high) gauge_layout.addLayout(markers_row) display_layout.addWidget(gauge_frame) # Row 3: Safe Low / Safe High bounds_row = QHBoxLayout() bounds_row.setSpacing(15) # Safe Low low_frame = QFrame() low_frame.setStyleSheet(""" QFrame { background-color: #111827; border-radius: 10px; border: none; } """) low_frame.setMinimumHeight(110) low_frame.setMinimumWidth(180) low_layout = QVBoxLayout(low_frame) low_layout.setContentsMargins(12, 12, 12, 12) low_layout.setSpacing(8) low_title = QLabel("⬇️ SAFE LOW") low_title.setStyleSheet(""" color: #ef4444; font-weight: 800; font-size: 10px; letter-spacing: 1px; background: rgba(239, 68, 68, 0.1); border: none; border-radius: 4px; padding: 4px; """) low_title.setAlignment(Qt.AlignmentFlag.AlignCenter) low_layout.addWidget(low_title) self.sr_low_label = QLabel("--") self.sr_low_label.setFont(QFont("Cascadia Code", 18, QFont.Weight.Bold)) self.sr_low_label.setStyleSheet("color: #ef4444; background: transparent; border: none;") self.sr_low_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.sr_low_label.setWordWrap(False) low_layout.addWidget(self.sr_low_label) bounds_row.addWidget(low_frame) # Safe High high_frame = QFrame() high_frame.setStyleSheet(""" QFrame { background-color: #111827; border-radius: 10px; border: none; } """) high_frame.setMinimumHeight(110) high_frame.setMinimumWidth(180) high_layout = QVBoxLayout(high_frame) high_layout.setContentsMargins(12, 12, 12, 12) high_layout.setSpacing(8) high_title = QLabel("⬆️ SAFE HIGH") high_title.setStyleSheet(""" color: #10b981; font-weight: 800; font-size: 10px; letter-spacing: 1px; background: rgba(16, 185, 129, 0.1); border: none; border-radius: 4px; padding: 4px; """) high_title.setAlignment(Qt.AlignmentFlag.AlignCenter) high_layout.addWidget(high_title) self.sr_high_label = QLabel("--") self.sr_high_label.setFont(QFont("Cascadia Code", 18, QFont.Weight.Bold)) self.sr_high_label.setStyleSheet("color: #10b981; background: transparent; border: none;") self.sr_high_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.sr_high_label.setWordWrap(False) high_layout.addWidget(self.sr_high_label) bounds_row.addWidget(high_frame) display_layout.addLayout(bounds_row) display_layout.addSpacing(15) # Row 4: Additional metrics metrics_row = QHBoxLayout() # Range Size self.sr_size_label = QLabel("📏 Range: -- pips") self.sr_size_label.setStyleSheet("color: #60a5fa; font-size: 12px;") metrics_row.addWidget(self.sr_size_label) # ATR self.sr_atr_label = QLabel("📊 ATR: --") self.sr_atr_label.setStyleSheet("color: #8b5cf6; font-size: 12px;") metrics_row.addWidget(self.sr_atr_label) # Confidence self.sr_conf_label = QLabel("✓ 99.9% Confidence") self.sr_conf_label.setStyleSheet("color: #059669; font-size: 12px; font-weight: bold;") metrics_row.addWidget(self.sr_conf_label) # Trend Certainty (TF/5) self.sr_certainty_label = QLabel("🎯 Certainty (TF/5): --") self.sr_certainty_label.setStyleSheet("color: #f59e0b; font-size: 12px; font-weight: bold;") metrics_row.addWidget(self.sr_certainty_label) metrics_row.addStretch() display_layout.addLayout(metrics_row) layout.addWidget(display_frame) # ═══════════════════════════════════════════════════════ # TRADING RECOMMENDATION # ═══════════════════════════════════════════════════════ self.sr_recommendation = QLabel("Select timeframe and click Refresh") self.sr_recommendation.setFont(QFont("Segoe UI", 12)) self.sr_recommendation.setStyleSheet(""" background-color: #3c3c3c; color: #cccccc; padding: 15px; border-radius: 8px; """) self.sr_recommendation.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.sr_recommendation) return tab def _on_sr_tf_click(self, tf: str): """Handle timeframe button click in Trading Operations tab""" # Update button styles for t, btn in self.sr_tf_buttons.items(): if t == tf: btn.set_color("#059669", "#047857", "#065f46") # Active: Green else: btn.set_color("#2d2d2d", "#3c3c3c", "#505050") # Inactive: Dark self._sr_selected_tf = tf self._refresh_safe_range() # Auto-refresh on click def _refresh_safe_range(self): """Manual refresh - Triggers Full AI Prediction""" self.sr_status.setText("⏳ Analyzing...") self.sr_status.setStyleSheet("color: #60a5fa;") tf = getattr(self, '_sr_selected_tf', '5m') # Start worker self.sr_worker = PredictionWorker(self.app_controller, tf) self.sr_worker.finished.connect(self._apply_prediction_success) self.sr_worker.error.connect(self._apply_safe_range_error) self.sr_worker.start() def _apply_safe_range_error(self, msg): self.sr_status.setText(f"❌ {msg}") self.sr_status.setStyleSheet("color: #ef4444;") self.sr_status.setToolTip(str(msg)) def _apply_prediction_success(self, result, tf): try: # 1. Update Safe Range Visuals (Noble KPI) # These keys come from the merged noble_result close_price = result.get('close_price', 0.0) position = result.get('position', 50.0) safe_low = result.get('safe_low', 0.0) safe_high = result.get('safe_high', 0.0) range_pips = result.get('range_pips', 0.0) atr = result.get('atr', 0.0) # Price Color if position < 30: price_color = "#10b981" # Bullish Zone elif position > 70: price_color = "#ef4444" # Bearish Zone else: price_color = "#ffffff" self.sr_price_label.setText(f"{close_price:.5f}") self.sr_price_label.setStyleSheet(f"color: {price_color};") self.sr_low_label.setText(f"{safe_low:.5f}") self.sr_high_label.setText(f"{safe_high:.5f}") self.sr_size_label.setText(f"📏 Range: {range_pips:.1f} pips") self.sr_atr_label.setText(f"📊 ATR: {atr:.5f}") # Position Bar pos_pct = int(position) bar_char = "█" * (pos_pct // 10) + "░" * (10 - pos_pct // 10) self.sr_position_label.setText(f"Position: [{bar_char}] {position:.1f}%") # Gauge Update if hasattr(self, 'sr_gauge'): self.sr_gauge.setValue(pos_pct) self.sr_gauge_low.setText(f"{safe_low:.5f}") self.sr_gauge_high.setText(f"{safe_high:.5f}") self.sr_gauge_current.setText(f"{close_price:.5f}") # K-Value / Regime regime = result.get('regime', 'NORMAL').upper() k_value = result.get('k_value', 1.0) self.sr_conf_label.setText(f"🔧 K={k_value:.1f} | {regime}") # 2. Update Prediction Signal (AI CONGRESS) # This overrides the simple Noble signal if available ai_signal = result.get('signal', 'NEUTRAL') confidence = result.get('confidence', 0.0) trend_certainty = result.get('trend_certainty', 0.0) duration_tf5 = result.get('duration_tf5', 0) # Use specific reason if available, else fallback to KPI description reason = result.get('reason', result.get('kpi_desc', 'AI Consensus')) # Update Certainty Label if trend_certainty > 0: self.sr_certainty_label.setText(f"🎯 Trend Strength: {trend_certainty:.0%} ({duration_tf5}m)") else: self.sr_certainty_label.setText("🎯 Certainty (TF/5): --") # Special case for skipped prediction status_msg = result.get('status', 'ok') if status_msg == 'market_closed': ai_signal = "NEUTRAL" reason = f"Market Closed ({result.get('reason', '')})" # Check for Sentinel Veto in details congress_data = result.get('congress_decision', {}) details = congress_data.get('details', {}) if congress_data else {} is_vetoed = details.get('veto') == 'sentinel_disagreement' original_signal = details.get('sentinel_signal', 'NEUTRAL') # Actually sentinel_signal is what Sentinel saw (e.g. SELL vs Core BUY). # Wait, if Core > 0.2 and Sentinel < -0.3, Sentinel checks SELL. The Trend was likely BUY (Core). # I stored core_signal_val in details too! core_val = details.get('core_signal_val', 0.0) trend_signal = "BUY" if core_val > 0.2 else ("SELL" if core_val < -0.2 else original_signal) if is_vetoed and ai_signal == "NEUTRAL": # MANUAL MODE: Show the Vetoed Trend reason = "Sentinel Veto (Manual Only)" # Use Orange/Yellow to indicate "Risky Trend" self.sr_recommendation.setText(f"⚠️ TREND {trend_signal} (Vetoed) - {reason}") self.sr_recommendation.setStyleSheet(""" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #78350f, stop:1 #92400e); color: #fbbf24; padding: 15px; border-radius: 8px; font-weight: bold; border: 2px solid #f59e0b; """) # Format Recommendation (Standard) elif ai_signal in ["STRONG_BUY", "BUY"]: self.sr_recommendation.setText(f"🟢 {ai_signal} ({confidence:.0%}) - {reason}") self.sr_recommendation.setStyleSheet(""" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #064e3b, stop:1 #065f46); color: #10b981; padding: 15px; border-radius: 8px; font-weight: bold; border: 2px solid #10b981; """) elif ai_signal in ["STRONG_SELL", "SELL"]: self.sr_recommendation.setText(f"🔴 {ai_signal} ({confidence:.0%}) - {reason}") self.sr_recommendation.setStyleSheet(""" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #7f1d1d, stop:1 #991b1b); color: #ef4444; padding: 15px; border-radius: 8px; font-weight: bold; border: 2px solid #ef4444; """) else: self.sr_recommendation.setText(f"⚪ {ai_signal} - {reason}") self.sr_recommendation.setStyleSheet(""" background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #374151, stop:1 #4b5563); color: #d1d5db; padding: 15px; border-radius: 8px; font-weight: bold; border: 1px solid #6b7280; """) # Update Timestamp last_time = result.get('last_update', 'Now') try: if hasattr(last_time, 'strftime'): last_time = last_time.strftime("%H:%M") else: last_time = str(last_time).split(' ')[-1][:5] except: pass self.sr_status.setText(f"✅ {tf} Updated ({last_time})") self.sr_status.setStyleSheet("color: #10b981;") except Exception as e: logger.error(f"Safe Range UI update error: {e}", exc_info=True) self.sr_status.setText("❌ Display Error") def _update_market_status(self): """Update Forex market status - OPEN/CLOSED detection (MT5 + Fallback)""" try: # 1. TRY MT5 REAL-TIME STATUS (Priority) if self.app_controller and self.app_controller.mt5_connected: m_status = self.app_controller.mt5.get_market_status() if m_status["status"] == "OPEN": server_time = m_status.get("server_time", "--:--") self.market_status.setText(f"● OPEN | {m_status['description']} 💹 • {server_time}") self.market_status.setStyleSheet("color: #10b981; font-weight: bold; font-size: 12px;") return elif m_status["status"] in ["CLOSED", "HOLIDAY"]: self.market_status.setText(f"● {m_status['status']} | {m_status['description']}") self.market_status.setStyleSheet("color: #ef4444; font-weight: bold; font-size: 12px;") return # If "CHECKING" or other, continue to fallback # 2. FALLBACK: ESTIMATED UTC LOGIC (Calculated) from datetime import datetime, timezone now_utc = datetime.now(timezone.utc) weekday = now_utc.weekday() hour = now_utc.hour month = now_utc.month day = now_utc.day # Holiday Check is_holiday = False holiday_name = "" if month == 1 and day == 1: is_holiday, holiday_name = True, "New Year's Day" elif month == 12 and day == 25: is_holiday, holiday_name = True, "Christmas Day" if is_holiday: self.market_status.setText(f"● CLOSED | {holiday_name} 🎉") self.market_status.setStyleSheet("color: #f59e0b; font-weight: bold; font-size: 12px;") return # Weekend Check (Friday 22:00 UTC to Sunday 22:00 UTC) is_open = True if weekday == 4 and hour >= 22: is_open = False elif weekday == 5: is_open = False elif weekday == 6 and hour < 22: is_open = False if is_open: self.market_status.setText(f"● OPEN | EST. Market Hours") self.market_status.setStyleSheet("color: #10b981; font-weight: 100; font-size: 11px;") else: self.market_status.setText(f"● CLOSED | Weekend Market") self.market_status.setStyleSheet("color: #ef4444; font-weight: bold; font-size: 12px;") except Exception as e: self.market_status.setText("⚠️ Status Error") self.market_status.setStyleSheet("color: #f59e0b;") @profile(threshold_ms=30.0) def _update_dashboard(self): """Update dashboard data - LIGHTWEIGHT (no blocking operations)""" if not self.app_controller or not self.app_controller.mt5: return # Only update lightweight elements (status, timestamps) try: self.last_update.setText(f"🕐 {datetime.now().strftime('%H:%M:%S')}") # Live Chart Update if active (Has its own internal 30s cooldown) if self.tabs.currentIndex() == 0 and hasattr(self, '_selected_tf'): if not getattr(self, '_tf_cooldown', False): self._live_update_chart() except: pass def _live_update_chart(self): """Fetch latest data for live chart - BACKGROUND THREAD with cooldown""" import time as _time # Cooldown check: only update every N seconds now = _time.time() if now - self._last_live_update_time < self._LIVE_UPDATE_INTERVAL_SEC: return # Skip, too soon if self._chart_updating: return # Skip if already updating self._chart_updating = True self._last_live_update_time = now def _fetch_and_update(): try: tf = getattr(self, '_selected_tf', '4h') # Fetch latest candles (reduced for live updates) df = self.app_controller.mt5.get_latest_candles(tf, n=200) if df is not None and not df.empty: # OPTIMIZATION: Check if data actually changed last_ts = df.index[-1] last_update = getattr(self, '_last_chart_ts', None) if last_update == last_ts: # Data hasn't changed, skip heavy UI update return self._last_chart_ts = last_ts # Schedule UI update on main thread QTimer.singleShot(0, lambda: self._apply_chart_update(df, tf)) except Exception as e: pass finally: self._chart_updating = False import threading threading.Thread(target=_fetch_and_update, daemon=True).start() @profile(threshold_ms=50.0) def _apply_chart_update(self, df, tf): """Apply chart data on main thread""" try: if hasattr(self, 'chart_widget') and df is not None: self.chart_widget.update_chart(df, tf) t = datetime.now().strftime("%H:%M:%S") if "LIVE" not in self.chart_status.text(): self.chart_status.setText(f"🟢 LIVE {tf}") self.chart_status.setStyleSheet("color: #10b981; font-weight: bold;") except: pass def _update_signal_display(self, df): """Update signal display for current chart timeframe""" try: current_tf = self.chart_tf_combo.currentText() if self.app_controller: signal = self.app_controller.get_signal(current_tf) if signal: self._set_signal_display(signal.get("signal", "NEUTRAL"), signal.get("confidence", 0)) self.last_update.setText(f"Last update: {datetime.now().strftime('%H:%M:%S')}") except Exception as e: logger.error(f"Signal display error: {e}") def _open_fullscreen_chart(self): """Open fullscreen chart with current timeframe data""" dialog = FullscreenChartDialog(self) # Load and pass OHLC data try: from config.settings import OHLC_DIR current_tf = getattr(self, '_selected_tf', '4h') ohlc_file = OHLC_DIR / f"ohlc_EURUSD_{current_tf}.csv" if ohlc_file.exists(): df = pd.read_csv(ohlc_file) if 'time' in df.columns: df['time'] = pd.to_datetime(df['time'], errors='coerce') df = df.dropna(subset=['time']) dialog.set_data(df, current_tf) except Exception as e: logger.error(f"Fullscreen chart error: {e}") dialog.exec() def _on_timeframe_changed(self, tf): self._update_dashboard() def _on_chart_tf_changed(self, tf: str): """Handle chart-specific timeframe change - non-blocking""" # Use deferred loading to avoid blocking UI QTimer.singleShot(50, lambda: self._load_chart_async(tf)) def _load_chart_async(self, tf: str): """Load chart data asynchronously""" try: from config.settings import OHLC_DIR ohlc_file = OHLC_DIR / f"ohlc_EURUSD_{tf}.csv" if ohlc_file.exists(): df = pd.read_csv(ohlc_file) if 'time' in df.columns: df['time'] = pd.to_datetime(df['time'], errors='coerce') df = df.dropna(subset=['time']) if hasattr(self, 'chart_widget') and len(df) > 0: self.chart_widget.update_chart(df, tf) except Exception as e: logger.error(f"Chart load error: {e}") def _set_signal_display(self, signal, confidence): self.signal_label.setText(signal) self.confidence_label.setText(f"Confidence: {confidence:.1%}") if signal == "BUY": color, bg = "#10b981", "rgba(16, 185, 129, 0.2)" elif signal == "SELL": color, bg = "#ef4444", "rgba(239, 68, 68, 0.2)" else: color, bg = "#6b7280", "rgba(107, 114, 128, 0.15)" self.signal_label.setStyleSheet(f"color: {color};") self.signal_label.parentWidget().setStyleSheet(f""" QFrame {{ background-color: {bg}; border: 3px solid {color}; border-radius: 15px; }} QLabel {{ background: transparent; border: none; }} """) # Simulator removed per user request def _create_calendar_tab(self): """Create Economic Calendar tab using CalendarWidget""" from gui.widgets.calendar_widget import CalendarWidget return CalendarWidget(self) def _create_mt5_info_tab(self): """Create MT5 Account Info tab with all account details and launch button""" tab = QWidget() layout = QVBoxLayout(tab) layout.setSpacing(15) layout.setContentsMargins(15, 15, 15, 15) # Header with title header = QHBoxLayout() title = QLabel("💰 MT5 ACCOUNT INFO") title.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold)) title.setStyleSheet("color: #60a5fa;") header.addWidget(title) header.addStretch() layout.addLayout(header) # Actions Row (New line after title) actions_layout = QHBoxLayout() actions_layout.addStretch() # PANIC BUTTON (CLOSE ALL) from gui.widgets.styled_button import Premium3DButton self.panic_btn = Premium3DButton("🚨 PANIC CLOSE", color="#dc2626", hover_color="#ef4444", pressed_color="#b91c1c") self.panic_btn.setFixedSize(160, 40) self.panic_btn.clicked.connect(self._panic_close_all) self.panic_btn.setToolTip("EMERGENCY: Close ALL Open Positions Immediately") actions_layout.addWidget(self.panic_btn) # Refresh Button refresh_btn = Premium3DButton("🔄 Refresh", color="#0e639c", hover_color="#1177bb", pressed_color="#094771") refresh_btn.setFixedSize(120, 40) refresh_btn.clicked.connect(self._refresh_mt5_info) actions_layout.addWidget(refresh_btn) # Launch MT5 Button launch_btn = Premium3DButton("🚀 Launch MT5", color="#059669", hover_color="#047857", pressed_color="#065f46") launch_btn.setFixedSize(140, 40) launch_btn.clicked.connect(self._launch_mt5) actions_layout.addWidget(launch_btn) layout.addLayout(actions_layout) # Account Info Frame info_frame = QFrame() info_frame.setMinimumHeight(200) info_frame.setStyleSheet(""" QFrame { background-color: #1e1e1e; border: 1px solid #3c3c3c; border-radius: 10px; } QLabel { background: transparent; border: none; } """) info_layout = QGridLayout(info_frame) info_layout.setSpacing(10) info_layout.setContentsMargins(20, 20, 20, 20) # Account Info Labels (2 columns layout) self.mt5_info_labels = {} fields = [ ("Login", "login"), ("Server", "server"), ("Name", "name"), ("Company", "company"), ("Currency", "currency"), ("Leverage", "leverage"), ("Balance", "balance"), ("Equity", "equity"), ("Profit", "profit"), ("Free Margin", "margin_free"), ("Margin Level", "margin_level"), ("Trade Mode", "trade_mode"), ] row = 0 col = 0 for label_text, key in fields: # Label (gray) lbl = QLabel(f"{label_text}:") lbl.setStyleSheet("color: #9ca3af; font-size: 12px;") info_layout.addWidget(lbl, row, col * 2) # Value (white, bold) val = QLabel("--") val.setStyleSheet("color: #ffffff; font-size: 14px; font-weight: bold;") self.mt5_info_labels[key] = val info_layout.addWidget(val, row, col * 2 + 1) col += 1 if col >= 2: col = 0 row += 1 layout.addWidget(info_frame) # Open Positions Section positions_header = QLabel("📊 Open Positions") positions_header.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold)) positions_header.setStyleSheet("color: #a78bfa; margin-top: 10px;") layout.addWidget(positions_header) self.mt5_positions_label = QLabel("Click 'Refresh' to load positions") self.mt5_positions_label.setStyleSheet(""" background-color: #1e1e1e; color: #cccccc; padding: 15px; border-radius: 8px; font-size: 12px; """) self.mt5_positions_label.setWordWrap(True) layout.addWidget(self.mt5_positions_label) layout.addStretch() # Auto-refresh timer for MT5 info self._mt5_info_timer = QTimer(self) self._mt5_info_timer.timeout.connect(self._refresh_mt5_info) self._mt5_info_timer.start(1000) # Refresh every 1 second (High Perf) # Initial load QTimer.singleShot(1000, self._refresh_mt5_info) return tab def _panic_close_all(self): """Handler for Panic Button""" if not self.app_controller or not self.app_controller.mt5: return from PyQt6.QtWidgets import QMessageBox reply = QMessageBox.question( self, "🚨 PANIC CLOSE CONFIRMATION", "ARE YOU SURE YOU WANT TO CLOSE ALL POSITIONS?\n\nThis will immediately liquidate all open trades at market price.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: logger.warning("🚨 USER TRIGGERED PANIC COMPLIANCE PROTOCOL") closed, errors = self.app_controller.mt5.close_all_positions() msg = f"Panic Protocol Executed.\nClosed: {closed}\nErrors: {errors}" if errors > 0: QMessageBox.critical(self, "Execution Report", msg) else: QMessageBox.information(self, "Execution Report", msg) # Immediate Refresh self._refresh_mt5_info() def _troubleshoot_connection(self): """Run MT5 diagnostic check""" if not self.app_controller: return self.troubleshoot_btn.setEnabled(False) self.troubleshoot_btn.setText("🔧 Checking...") # AppController.check_mt5_health takes no arguments and returns (success, message) success, message = self.app_controller.check_mt5_health() from PyQt6.QtWidgets import QMessageBox if success: QMessageBox.information(self, "MT5 Diagnostic", f"✅ {message}") else: QMessageBox.critical(self, "MT5 Diagnostic", f"❌ {message}\n\nTip: ensure MetaTrader 5 is open and 'Algo Trading' is enabled.") self.troubleshoot_btn.setEnabled(True) self.troubleshoot_btn.setText("🔧 Troubleshoot Connection") def _launch_mt5(self): """Launch MetaTrader 5 terminal""" import subprocess import os # Common MT5 installation paths mt5_paths = [ r"C:\Program Files\MetaTrader 5\terminal64.exe", r"C:\Program Files (x86)\MetaTrader 5\terminal64.exe", os.path.expanduser(r"~\AppData\Roaming\MetaQuotes\Terminal\*\terminal64.exe"), ] # Try to find MT5 for path in mt5_paths: if "*" in path: import glob matches = glob.glob(path) if matches: path = matches[0] if os.path.exists(path): try: subprocess.Popen([path]) logger.info(f"Launched MT5 from: {path}") return except Exception as e: logger.error(f"Failed to launch MT5: {e}") # Fallback: Try to find in registry or just open with shell try: os.startfile("terminal64.exe") except: logger.warning("Could not auto-launch MT5. Please open it manually.") def _refresh_mt5_info(self): """Refresh MT5 account information""" if not self.app_controller: return try: info = self.app_controller.get_cached_account_info() if info: # Update labels self.mt5_info_labels["login"].setText(str(info.get("login", "--"))) self.mt5_info_labels["server"].setText(str(info.get("server", "--"))) self.mt5_info_labels["name"].setText(str(info.get("name", "--"))) self.mt5_info_labels["company"].setText(str(info.get("company", "--"))) self.mt5_info_labels["currency"].setText(str(info.get("currency", "--"))) self.mt5_info_labels["leverage"].setText(f"1:{info.get('leverage', '--')}") balance = info.get("balance", 0) equity = info.get("equity", 0) profit = info.get("profit", 0) margin_free = info.get("margin_free", 0) margin_level = info.get("margin_level", 0) currency = info.get("currency", "USD") self.mt5_info_labels["balance"].setText(f"{balance:,.2f} {currency}") self.mt5_info_labels["equity"].setText(f"{equity:,.2f} {currency}") # Color profit based on value if profit >= 0: self.mt5_info_labels["profit"].setText(f"+{profit:,.2f} {currency}") self.mt5_info_labels["profit"].setStyleSheet("color: #10b981; font-size: 16px; font-weight: bold;") else: self.mt5_info_labels["profit"].setText(f"{profit:,.2f} {currency}") self.mt5_info_labels["profit"].setStyleSheet("color: #ef4444; font-size: 16px; font-weight: bold;") self.mt5_info_labels["margin_free"].setText(f"{margin_free:,.2f} {currency}") # Smart Margin Level Display if margin_level > 0: self.mt5_info_labels["margin_level"].setText(f"{margin_level:.2f}%") # Color code risk if margin_level < 100: self.mt5_info_labels["margin_level"].setStyleSheet("color: #ef4444; font-weight: bold;") # Red else: self.mt5_info_labels["margin_level"].setStyleSheet("color: #10b981; font-weight: bold;") # Green else: # If margin level is 0, usually means no margin used (Safe) self.mt5_info_labels["margin_level"].setText("Safe (No Use)" if equity > 0 else "0.00%") self.mt5_info_labels["margin_level"].setStyleSheet("color: #6b7280;") # Trade mode trade_mode_map = {0: "Demo", 1: "Contest", 2: "Real"} trade_mode = info.get("trade_mode", 0) self.mt5_info_labels["trade_mode"].setText(trade_mode_map.get(trade_mode, str(trade_mode))) # Update positions positions = info.get("positions", []) if positions: pos_text = "" for pos in positions[:10]: # Limit to 10 symbol = pos.get("symbol", "?") volume = pos.get("volume", 0) profit = pos.get("profit", 0) direction = "🟢 BUY" if pos.get("type", 0) == 0 else "🔴 SELL" pos_text += f"{direction} {symbol} | {volume} lots | P/L: {profit:+.2f}\n" self.mt5_positions_label.setText(pos_text.strip()) else: self.mt5_positions_label.setText("No open positions") else: self.mt5_positions_label.setText("⚠️ MT5 not connected or data unavailable") except Exception as e: logger.debug(f"MT5 info refresh error: {e}") # NOTE: _on_sim_tf_click is defined earlier at lines 1088-1245 (complete version) def _create_scheduler_group(self): """Create AI Scheduler Control Center (PRO UI)""" container = QWidget() layout = QVBoxLayout(container) layout.setSpacing(20) layout.setContentsMargins(30, 30, 30, 30) # 1. Main Control Card card = QFrame() card.setStyleSheet(""" QFrame { background-color: #1e1e1e; border-radius: 15px; border: none; } """) card_layout = QVBoxLayout(card) card_layout.setSpacing(15) card_layout.setContentsMargins(20, 20, 20, 20) # Header Row (Title + Toggle) header_layout = QHBoxLayout() title_icon = QLabel("🧠") title_icon.setFont(QFont("Segoe UI Emoji", 24)) header_layout.addWidget(title_icon) title_col = QVBoxLayout() title_col.setSpacing(2) title_lbl = QLabel("Neural Auto-Pilot") title_lbl.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold)) title_lbl.setStyleSheet("color: white;") subtitle_lbl = QLabel("Automated retraining system") subtitle_lbl.setFont(QFont("Segoe UI", 10)) subtitle_lbl.setStyleSheet("color: #9ca3af;") title_col.addWidget(title_lbl) title_col.addWidget(subtitle_lbl) header_layout.addLayout(title_col) header_layout.addStretch() # Toggle Switch self.sched_switch = QCheckBox("DISABLED") self.sched_switch.setCursor(Qt.CursorShape.PointingHandCursor) self.sched_switch.setStyleSheet(""" QCheckBox { color: #ef4444; font-weight: bold; font-size: 14px; padding: 5px; } QCheckBox::indicator { width: 60px; height: 30px; background-color: #374151; border-radius: 15px; } QCheckBox::indicator:checked { background-color: #10b981; } QCheckBox::indicator:unchecked { background-color: #374151; } """) # Load state if self.app_controller: self.sched_switch.setChecked(self.app_controller.scheduler_enabled) if self.app_controller.scheduler_enabled: self.sched_switch.setText("ACTIVE") self.sched_switch.setStyleSheet(self.sched_switch.styleSheet().replace("color: #ef4444", "color: #10b981")) self.sched_switch.toggled.connect(self._on_scheduler_toggle) header_layout.addWidget(self.sched_switch) card_layout.addLayout(header_layout) # Divider line = QFrame() line.setFrameShape(QFrame.Shape.HLine) line.setStyleSheet("color: #333333;") card_layout.addWidget(line) # Breakdown Row (Status + Timer) status_layout = QHBoxLayout() # Left: Cycle Config config_col = QVBoxLayout() config_lbl = QLabel("RETRAINING CYCLE") config_lbl.setFont(QFont("Segoe UI", 9, QFont.Weight.Bold)) config_lbl.setStyleSheet("color: #6b7280; letter-spacing: 1px;") config_col.addWidget(config_lbl) self.sched_cycle = QComboBox() self.sched_cycle.addItems(["4h", "12h", "Daily", "Weekly"]) self.sched_cycle.setFixedWidth(200) self.sched_cycle.setFixedHeight(40) self.sched_cycle.setStyleSheet(""" QComboBox { background-color: #252526; color: white; border: 1px solid #4b5563; padding: 10px; border-radius: 6px; font-size: 14px; font-weight: bold; } QComboBox::drop-down { border: none; width: 30px; } QComboBox::down-arrow { image: none; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #4ade80; margin-right: 10px; } QComboBox QAbstractItemView { background-color: #252526; color: white; selection-background-color: #374151; } """) if self.app_controller: idx = self.sched_cycle.findText(self.app_controller.scheduler_cycle, Qt.MatchFlag.MatchFixedString) if idx >= 0: self.sched_cycle.setCurrentIndex(idx) self.sched_cycle.currentIndexChanged.connect(self._on_scheduler_cycle_change) config_col.addWidget(self.sched_cycle) status_layout.addLayout(config_col) status_layout.addStretch() # Right: Countdown timer_col = QVBoxLayout() timer_lbl = QLabel("NEXT EXECUTION IN") timer_lbl.setFont(QFont("Segoe UI", 9, QFont.Weight.Bold)) timer_lbl.setStyleSheet("color: #6b7280; letter-spacing: 1px;") timer_lbl.setAlignment(Qt.AlignmentFlag.AlignRight) timer_col.addWidget(timer_lbl) self.sched_countdown = QLabel("--:--:--") self.sched_countdown.setFont(QFont("Consolas", 28, QFont.Weight.Bold)) self.sched_countdown.setStyleSheet("color: #60a5fa;") self.sched_countdown.setAlignment(Qt.AlignmentFlag.AlignRight) timer_col.addWidget(self.sched_countdown) status_layout.addLayout(timer_col) card_layout.addLayout(status_layout) # Progress Bar self.sched_progress = QProgressBar() self.sched_progress.setFixedHeight(8) self.sched_progress.setTextVisible(False) self.sched_progress.setStyleSheet(""" QProgressBar { background-color: #2d2d2d; border-radius: 4px; } QProgressBar::chunk { background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 #3b82f6, stop:1 #8b5cf6); border-radius: 4px; } """) self.sched_progress.setValue(0) card_layout.addWidget(self.sched_progress) layout.addWidget(card) # 2. Console / Logs (Mini terminal) log_frame = QFrame() log_frame.setStyleSheet(""" QFrame { background-color: #000000; border-radius: 8px; border: none; } """) log_layout = QVBoxLayout(log_frame) log_header = QLabel("> SYSTEM LOGS") log_header.setFont(QFont("Consolas", 10, QFont.Weight.Bold)) log_header.setStyleSheet("color: #10b981;") log_layout.addWidget(log_header) self.sched_log = QLabel("System initialized.\nWaiting for scheduler activation...") self.sched_log.setFont(QFont("Consolas", 10)) self.sched_log.setStyleSheet("color: #cccccc;") self.sched_log.setWordWrap(True) log_layout.addWidget(self.sched_log) layout.addWidget(log_frame) # 3. Actions btn_layout = QHBoxLayout() btn_layout.addStretch() self.run_now_btn = QPushButton("▶ FORCE RUN NOW") self.run_now_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.run_now_btn.setFixedSize(160, 40) self.run_now_btn.setStyleSheet(""" QPushButton { background-color: #374151; color: white; border: none; border-radius: 6px; font-weight: bold; } QPushButton:hover { background-color: #4b5563; } QPushButton:pressed { background-color: #1f2937; } """) self.run_now_btn.clicked.connect(self._on_force_run_click) btn_layout.addWidget(self.run_now_btn) layout.addLayout(btn_layout) layout.addStretch() return container def _on_scheduler_toggle(self, checked): """Handle ON/OFF toggle""" if checked: self.sched_switch.setText("ACTIVE") self.sched_switch.setStyleSheet(self.sched_switch.styleSheet().replace("color: #ef4444", "color: #10b981")) self.sched_log.setText(self.sched_log.text() + "\n> Scheduler ACTIVATED.") else: self.sched_switch.setText("DISABLED") self.sched_switch.setStyleSheet(self.sched_switch.styleSheet().replace("color: #10b981", "color: #ef4444")) self.sched_log.setText(self.sched_log.text() + "\n> Scheduler DEACTIVATED.") self.sched_countdown.setText("--:--:--") self.sched_progress.setValue(0) # Update Controller if self.app_controller: cycle = self.sched_cycle.currentText().lower() self.app_controller.update_scheduler_config(checked, cycle) self._update_scheduler_ui() def _on_scheduler_cycle_change(self): """Handle cycle change""" if self.app_controller: checked = self.sched_switch.isChecked() cycle = self.sched_cycle.currentText().lower() self.sched_log.setText(self.sched_log.text() + f"\n> Cycle changed to {cycle}.") self.app_controller.update_scheduler_config(checked, cycle) self._update_scheduler_ui() def _on_force_run_click(self): """Force run now""" self.sched_log.setText(self.sched_log.text() + "\n> FORCE RUN INITIATED...") if self.app_controller: self.app_controller.train_all() def _update_scheduler_ui(self): """Update countdown label and progress""" if not self.app_controller: return try: if self.app_controller.scheduler_enabled and self.app_controller.next_run_time: now = datetime.now() target = self.app_controller.next_run_time diff = target - now total_seconds = diff.total_seconds() if total_seconds < 0: self.sched_countdown.setText("RUNNING...") self.sched_progress.setValue(self.sched_progress.maximum()) else: hours = int(total_seconds // 3600) mins = int((total_seconds % 3600) // 60) secs = int(total_seconds % 60) self.sched_countdown.setText(f"{hours:02}:{mins:02}:{secs:02}") # Progress Bar Logic cycle_map = {"4h": 4*3600, "12h": 12*3600, "daily": 24*3600, "weekly": 7*24*3600} cycle_sec = cycle_map.get(self.app_controller.scheduler_cycle, 4*3600) # Use seconds for smooth progress self.sched_progress.setRange(0, cycle_sec) elapsed = cycle_sec - int(total_seconds) self.sched_progress.setValue(max(0, min(cycle_sec, elapsed))) # Tooltip to explain alignment start_time = target - timedelta(seconds=cycle_sec) self.sched_progress.setToolTip(f"Cycle: {start_time.strftime('%H:%M')}{target.strftime('%H:%M')} (Aligned to Market Hours)") else: self.sched_countdown.setText("--:--:--") self.sched_progress.setValue(0) self.sched_progress.setToolTip("") except Exception as e: logging.error(f"Scheduler UI Update Error: {e}")