Source code for gui.main_window

"""
Cerebrum Forex - Main Window
Eclipse-like IDE interface with dockable panels.
"""

import logging
import sys
from typing import Optional

from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QDockWidget, QWidget, QVBoxLayout,
    QHBoxLayout, QMenuBar, QMenu, QToolBar, QStatusBar, QLabel,
    QPushButton, QTabWidget, QSplitter, QFrame, QSizePolicy
)
from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QUrl
from PyQt6.QtGui import QAction, QIcon, QFont, QPixmap, QDesktopServices

from gui.widgets.styled_button import Premium3DButton

from gui.panels.dashboard_panel import DashboardPanel
from gui.panels.extraction_panel import ExtractionPanel
from gui.panels.indicators_panel import IndicatorsPanel
# from gui.panels.features_panel import FeaturesPanel  # REMOVED: Not used in training/prediction
from gui.panels.training_panel import TrainingPanel
from gui.panels.prediction_panel import PredictionPanel
from gui.panels.settings_panel import SettingsPanel

from config.settings import get_resource_path


logger = logging.getLogger(__name__)


from core.profiler import profile

[docs] class MainWindow(QMainWindow): """Main application window with Eclipse-like layout""" # Signals for status updates status_changed = pyqtSignal(str) def __init__(self, app_controller=None): super().__init__() self.app_controller = app_controller self.is_active = False self._init_ui() # self._init_menu() # Removed as per request self._init_toolbar() # self._init_panels() # Moved to lazy loading self._init_statusbar() # Apply dark theme self._apply_dark_theme() # Start status timer self._status_timer = QTimer() self._status_timer.timeout.connect(self._update_status) self._status_timer.start(1000) # LAZY LOAD: Show window first, then load heavy panels QTimer.singleShot(100, self._lazy_load_panels) def _init_ui(self): """Initialize main UI""" self.setWindowTitle("Cerebrum Forex - AI Trading Signals (v1.2.1)") self.setMinimumSize(1000, 500) self.resize(1100, 560) # Center on screen screen = self.screen().availableGeometry() x = (screen.width() - self.width()) // 2 y = (screen.height() - self.height()) // 2 self.move(x, y) # Set window icon (prefer .ico for Windows, fallback to .jpg) icon_path = get_resource_path("resources/icon.ico") if not icon_path.exists(): icon_path = get_resource_path("assets/logo.jpg") if not icon_path.exists(): icon_path = get_resource_path("resources/logo.jpg") if icon_path.exists(): self.setWindowIcon(QIcon(str(icon_path))) # Central widget self.central_widget = QWidget() self.setCentralWidget(self.central_widget) # Main layout self.main_layout = QVBoxLayout(self.central_widget) self.main_layout.setContentsMargins(0, 0, 0, 0) self.main_layout.setSpacing(0) # Loading Overlay (Initial State) self.loading_container = QWidget() self.loading_layout = QVBoxLayout(self.loading_container) self.loading_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) self.loading_layout.setSpacing(20) # Logo logo_path = get_resource_path("assets/logo.jpg") if logo_path.exists(): self.logo_label = QLabel() pixmap = QPixmap(str(logo_path)) if not pixmap.isNull(): # Scale nicely if too big scaled_pixmap = pixmap.scaled(300, 300, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) self.logo_label.setPixmap(scaled_pixmap) self.logo_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.loading_layout.addWidget(self.logo_label) # Text self.loading_label = QLabel("🚀 Initializing Neural Core...\n(Loading 1.6GB Memory)") self.loading_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.loading_label.setStyleSheet("font-size: 18px; color: #60a5fa; font-weight: bold;") self.loading_layout.addWidget(self.loading_label) self.main_layout.addWidget(self.loading_container) def _init_menu(self): """Initialize menu bar""" menubar = self.menuBar() # File menu file_menu = menubar.addMenu("File") exit_action = QAction("Exit", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # Application menu app_menu = menubar.addMenu("Application") extract_all_action = QAction("Extract All OHLC", self) extract_all_action.triggered.connect(self._extract_all) app_menu.addAction(extract_all_action) train_all_action = QAction("Train All Models", self) train_all_action.triggered.connect(self._train_all) app_menu.addAction(train_all_action) predict_all_action = QAction("Predict All", self) predict_all_action.triggered.connect(self._predict_all) app_menu.addAction(predict_all_action) # View menu view_menu = menubar.addMenu("View") self.panel_actions = {} for panel_name in ["Dashboard", "Extraction", "Training", "Prediction", "Settings"]: action = QAction(panel_name, self) action.setCheckable(True) action.setChecked(True) view_menu.addAction(action) self.panel_actions[panel_name] = action # Help menu help_menu = menubar.addMenu("Help") docs_action = QAction("📖 Documentation", self) docs_action.triggered.connect(self._open_docs) help_menu.addAction(docs_action) help_menu.addSeparator() about_action = QAction("About", self) about_action.triggered.connect(self._show_about) help_menu.addAction(about_action) def _init_toolbar(self): """Initialize toolbar""" toolbar = QToolBar("Main Toolbar") toolbar.setMovable(False) self.addToolBar(toolbar) # Status indicator - always active since system auto-starts self.status_indicator = QLabel("● ACTIVE") self.status_indicator.setStyleSheet("color: #4ecca3; font-weight: bold; padding: 8px; background: transparent;") toolbar.addWidget(self.status_indicator) # Performance Mode Label (Red as requested) self.mode_label = QLabel("[PRO]") self.mode_label.setStyleSheet("color: #ff4d4d; font-weight: bold; padding: 8px; background: transparent;") self.mode_label.setToolTip( "<b>ECO</b>: Low RAM/CPU. Updates every 60s.<br>" "<b>BALANCED</b>: Standard performance. Updates every 10s.<br>" "<b>PRO</b>: High performance. Updates every 3s." ) toolbar.addWidget(self.mode_label) # Signal Indicator self.signal_label = QLabel("") self.signal_label.setStyleSheet("color: #6b7280; font-weight: bold; padding: 0 10px; font-size: 14px; background: transparent;") self.signal_label.hide() toolbar.addWidget(self.signal_label) self.confidence_label = QLabel("") self.confidence_label.setStyleSheet("color: #9ca3af; padding: 0 5px; background: transparent;") self.confidence_label.hide() toolbar.addWidget(self.confidence_label) # Spacer to push buttons to the right spacer = QWidget() spacer.setStyleSheet("background: transparent;") spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) toolbar.addWidget(spacer) # System Info self.cpu_label = QLabel("CPU: --%") self.cpu_label.setMinimumWidth(80) self.cpu_label.setStyleSheet("color: #cccccc; font-weight: bold; padding: 0 10px; background: transparent;") toolbar.addWidget(self.cpu_label) self.memory_label = QLabel("RAM: --%") self.memory_label.setMinimumWidth(80) self.memory_label.setStyleSheet("color: #cccccc; font-weight: bold; padding: 0 10px; background: transparent;") toolbar.addWidget(self.memory_label) # Exit Button exit_btn = Premium3DButton("Exit", color="#dc2626", hover_color="#ef4444", pressed_color="#b91c1c") exit_btn.clicked.connect(self.close) toolbar.addWidget(exit_btn) # Help Button help_btn = Premium3DButton("Help", color="#3c3c3c", hover_color="#505050", pressed_color="#2d2d2d") help_btn.clicked.connect(self._show_about) toolbar.addWidget(help_btn) # Docs Button (Blue) docs_btn = Premium3DButton("Docs", color="#2563eb", hover_color="#3b82f6", pressed_color="#1d4ed8") docs_btn.clicked.connect(self._open_docs) toolbar.addWidget(docs_btn) # Removed CPU/RAM as per request "apres de 'active' 2 btn : Help et Exit" def _lazy_load_panels(self): """Called after window show to load heavy UI without freezing initial paint""" try: self._init_panels() # Remove loading overlay self.loading_container.hide() self.main_layout.removeWidget(self.loading_container) self.loading_container.deleteLater() # Show actual content if hasattr(self, 'main_splitter'): self.main_splitter.show() # --- INITIAL GUIDANCE (Replacement for Welcome Pop-up) --- from config.settings import MODELS_DIR is_empty = not any(MODELS_DIR.glob("*.pkl")) if is_empty: self.statusbar.showMessage("👋 Welcome! Start by 'Extraction' then 'Training' to build your AI models.", 15000) else: self.statusbar.showMessage("🚀 Ready. Neural Core Loaded.", 5000) logger.info("Lazy loading complete. UI Ready.") except Exception as e: logger.critical(f"Lazy Load Failed: {e}", exc_info=True) self.loading_label.setText(f"❌ Startup Error:\n{e}") self.loading_label.setStyleSheet("font-size: 16px; color: #ef4444; font-weight: bold;") def _init_panels(self): """Initialize panels in Eclipse-like layout""" # Create main splitter (draggable) - directly in main layout, no tabs self.main_splitter = QSplitter(Qt.Orientation.Horizontal) self.main_splitter.setChildrenCollapsible(False) # Prevent collapsing self.main_splitter.setHandleWidth(5) # Make handle wider for easier dragging self.main_layout.addWidget(self.main_splitter) # Left panel - Dashboard (35%) self.dashboard_panel = DashboardPanel(self.app_controller) self.dashboard_panel.setMinimumWidth(300) # Right panel - Tabs for other sections (30%) self.right_tabs = QTabWidget() self.right_tabs.setTabPosition(QTabWidget.TabPosition.North) self.right_tabs.setMinimumWidth(300) # Create tab panels self.extraction_panel = ExtractionPanel(self.app_controller) self.indicators_panel = IndicatorsPanel(self.app_controller) # self.features_panel = FeaturesPanel(self.app_controller) # REMOVED self.training_panel = TrainingPanel(self.app_controller) self.prediction_panel = PredictionPanel(self.app_controller) self.prediction_panel.prediction_updated.connect(self.update_signal_display) self.settings_panel = SettingsPanel(self.app_controller) from gui.panels.testing_panel import TestingPanel self.testing_panel = TestingPanel(self.app_controller) # Connect Profile Constraints self.settings_panel.profile_changed.connect(self.extraction_panel.update_profile_constraints) # Initialize constraints if self.app_controller: current_profile = self.app_controller.get_settings().get('performance_profile', 'BALANCED') self.extraction_panel.update_profile_constraints(current_profile) # Add tabs self.right_tabs.addTab(self.extraction_panel, "📊 Extraction") self.right_tabs.addTab(self.indicators_panel, "📈 Indicators") # self.right_tabs.addTab(self.features_panel, "📚 Features") # REMOVED self.right_tabs.addTab(self.training_panel, "🧠 Training") self.right_tabs.addTab(self.prediction_panel, "🎯 Prediction") self.right_tabs.addTab(self.testing_panel, "🧪 Test Center") self.right_tabs.addTab(self.settings_panel, "⚙️ Settings") # Add to splitter self.main_splitter.addWidget(self.dashboard_panel) self.main_splitter.addWidget(self.right_tabs) # Log tab switching def log_tab_switch(index): tab_name = self.right_tabs.tabText(index) logger.info(f"═══ UI SECTION SWITCH: {tab_name} ═══") self.right_tabs.currentChanged.connect(log_tab_switch) # Set default split (40% Left, 60% Right) # Use a slightly longer delay to ensure window is visible QTimer.singleShot(500, lambda: self.main_splitter.setSizes([450, 650])) self.main_splitter.setStretchFactor(0, 40) self.main_splitter.setStretchFactor(1, 60) def _init_statusbar(self): """Initialize status bar""" self.statusbar = QStatusBar() self.setStatusBar(self.statusbar) # Status labels self.mt5_status = QLabel("MT5: Disconnected") self.mt5_status.setStyleSheet("color: #aaa; background: transparent;") self.statusbar.addWidget(self.mt5_status) separator1 = QLabel("|") separator1.setStyleSheet("background: transparent;") self.statusbar.addWidget(separator1) self.training_status = QLabel("Training: Idle") self.training_status.setStyleSheet("color: #aaa; background: transparent;") self.statusbar.addWidget(self.training_status) separator2 = QLabel("|") separator2.setStyleSheet("background: transparent;") self.statusbar.addWidget(separator2) self.prediction_status = QLabel("Prediction: Idle") self.prediction_status.setStyleSheet("color: #aaa; background: transparent;") self.statusbar.addWidget(self.prediction_status) def _apply_dark_theme(self): """Apply dark Eclipse-like theme using centralized theme constants""" from gui.theme import MainWindowStyles self.setStyleSheet(MainWindowStyles.DARK_THEME) def _extract_all(self): """Trigger extraction for all timeframes""" if self.app_controller: self.app_controller.extract_all() self.statusbar.showMessage("Extraction started...", 3000) def _train_all(self): """Trigger training for all timeframes""" if self.app_controller: self.app_controller.train_all() self.statusbar.showMessage("Training started...", 3000) def _predict_all(self): """Trigger prediction for all timeframes""" if self.app_controller: self.app_controller.predict_all() self.statusbar.showMessage("Prediction started...", 3000)
[docs] def update_signal_display(self, timeframe: str, signal: str, confidence: float): """Update toolbar signal display""" if not signal or signal == "NEUTRAL" and confidence == 0: self.signal_label.hide() self.confidence_label.hide() return self.signal_label.show() self.confidence_label.show() self.signal_label.setText(f"{timeframe}: {signal}") self.confidence_label.setText(f"{confidence*100:.1f}% Confidence") if signal == "STRONG BUY" or signal == "BUY": self.signal_label.setStyleSheet("color: #10b981; font-weight: bold; padding: 0 10px; font-size: 14px; background: transparent;") elif signal == "STRONG SELL" or signal == "SELL": self.signal_label.setStyleSheet("color: #ef4444; font-weight: bold; padding: 0 10px; font-size: 14px; background: transparent;") else: self.signal_label.setStyleSheet("color: #6b7280; font-weight: bold; padding: 0 10px; font-size: 14px; background: transparent;") # Watchdog Timer (Debug Freeze) self._watchdog_timer = QTimer() self._watchdog_timer.timeout.connect(self._watchdog_tick) self._watchdog_timer.start(1000)
def _watchdog_tick(self): """Watchdog to ensure UI stays responsive""" if self.app_controller and self.app_controller.is_active: # Basic liveness check could go here pass @profile(threshold_ms=20.0) def _update_status(self): """Update status bar info""" if not self.app_controller: return try: # 1. Update Mode Label (Red as requested) from config.settings import default_settings profile = getattr(default_settings, "performance_profile", "BALANCED") self.mode_label.setText(f"[{profile}]") # 2. Update MT5 status if self.app_controller.mt5_connected: self.mt5_status.setText("MT5: Connected") self.mt5_status.setStyleSheet("color: #4ecca3; background: transparent;") else: self.mt5_status.setText("MT5: Disconnected") self.mt5_status.setStyleSheet("color: #ff6b6b; background: transparent;") # 3. Update training status if self.app_controller.is_training: self.training_status.setText("Training: Running") self.training_status.setStyleSheet("color: #ffd93d; background: transparent;") elif self.app_controller.scheduler_enabled: self.training_status.setText("Training: Standby") self.training_status.setStyleSheet("color: #4ecca3; background: transparent;") else: self.training_status.setText("Training: Ready (Manual)") self.training_status.setStyleSheet("color: #60a5fa; background: transparent;") # 4. Update prediction status if self.app_controller.is_predicting: self.prediction_status.setText("Prediction: Running") self.prediction_status.setStyleSheet("color: #ffd93d; background: transparent;") elif self.app_controller.scheduler_enabled: self.prediction_status.setText("Prediction: Standby") self.prediction_status.setStyleSheet("color: #4ecca3; background: transparent;") else: self.prediction_status.setText("Prediction: Ready (Manual)") self.prediction_status.setStyleSheet("color: #60a5fa; background: transparent;") # 5. Update CPU and Memory import psutil cpu = psutil.cpu_percent() mem = psutil.virtual_memory().percent self.cpu_label.setText(f"CPU: {int(cpu):03d}%") self.memory_label.setText(f"RAM: {int(mem):03d}%") except Exception as e: logger.debug(f"Status update error: {e}") def _open_docs(self): """Open documentation in system browser (for better CSS support)""" from config.settings import get_resource_path from pathlib import Path # Sphinx documentation path (dist/_internal/docs/_build/html/index.html) docs_internal = get_resource_path("docs") / "_build" / "html" / "index.html" docs_root = Path.cwd() / "docs" / "_build" / "html" / "index.html" # Try internal first, then local dev path if docs_internal.exists(): docs_path = docs_internal elif docs_root.exists(): docs_path = docs_root elif (get_resource_path("docs")).exists(): # Just open the folder if html index not found docs_path = get_resource_path("docs") elif (Path.cwd() / "docs").exists(): docs_path = Path.cwd() / "docs" else: from PyQt6.QtWidgets import QMessageBox QMessageBox.warning( self, "Documentation", f"Documentation not found.\n\n" f"Checked:\n" f"• {docs_internal}\n" f"• {docs_root}" ) return # Open in system browser or explorer from PyQt6.QtCore import QUrl from PyQt6.QtGui import QDesktopServices QDesktopServices.openUrl(QUrl.fromLocalFile(str(docs_path.absolute()))) self.statusbar.showMessage("Opening documentation...", 3000) def _show_about(self): """Show about dialog""" from PyQt6.QtWidgets import QMessageBox QMessageBox.about( self, "About Cerebrum Forex", "Cerebrum Forex v1.2.1\n\n" "AI-powered trading signal platform.\n\n" "Models: XGBoost, LightGBM, RandomForest, Stacking\n" "Timeframes: 1m, 5m, 15m, 30m, 1H, 4H\n\n" "© 2026 bmz business" )
[docs] def closeEvent(self, event): """Handle window close""" if self.app_controller: self.app_controller.stop() event.accept()