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