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