237 lines
8.1 KiB
Python
237 lines
8.1 KiB
Python
from __future__ import annotations
|
|
import os
|
|
from PyQt6.QtWidgets import (
|
|
QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
|
|
QLabel, QPushButton, QStackedWidget, QSizePolicy, QStatusBar,
|
|
QApplication
|
|
)
|
|
from PyQt6.QtCore import Qt, QTimer, pyqtSignal
|
|
from PyQt6.QtGui import QFont, QIcon
|
|
|
|
NAV_ITEMS = [
|
|
("dashboard", "⊞", "Dashboard"),
|
|
("chart", "📈", "Chart"),
|
|
("watchlist", "★", "Watchlist"),
|
|
("portfolio", "💼", "Portfolio"),
|
|
("ai", "✦", "AI"),
|
|
("news", "📰", "News"),
|
|
("screener", "⚙", "Screener"),
|
|
("settings", "⚙", "Settings"),
|
|
]
|
|
|
|
|
|
class SidebarButton(QPushButton):
|
|
def __init__(self, icon_text: str, tooltip: str, parent=None):
|
|
super().__init__(icon_text, parent)
|
|
self.setCheckable(True)
|
|
self.setFixedSize(44, 44)
|
|
self.setToolTip(tooltip)
|
|
self.setFont(QFont("Segoe UI Emoji", 14))
|
|
|
|
|
|
class MainWindow(QMainWindow):
|
|
def __init__(self, config):
|
|
super().__init__()
|
|
self._config = config
|
|
self._nav_buttons: dict[str, SidebarButton] = {}
|
|
self._alert_worker = None
|
|
self._setup_status_bar()
|
|
self._setup_ui()
|
|
self._start_alerts()
|
|
|
|
def _setup_ui(self):
|
|
self.setWindowTitle("StockMind")
|
|
self.setMinimumSize(1200, 700)
|
|
self.resize(1440, 860)
|
|
|
|
central = QWidget()
|
|
self.setCentralWidget(central)
|
|
layout = QHBoxLayout(central)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(0)
|
|
|
|
# Sidebar
|
|
sidebar = self._build_sidebar()
|
|
layout.addWidget(sidebar)
|
|
|
|
# Content stack
|
|
self._stack = QStackedWidget()
|
|
layout.addWidget(self._stack)
|
|
|
|
self._build_pages()
|
|
self._set_page("dashboard")
|
|
|
|
def _build_sidebar(self) -> QWidget:
|
|
sidebar = QWidget()
|
|
sidebar.setObjectName("sidebar")
|
|
sidebar.setFixedWidth(56)
|
|
sb_layout = QVBoxLayout(sidebar)
|
|
sb_layout.setContentsMargins(6, 8, 6, 8)
|
|
sb_layout.setSpacing(4)
|
|
sb_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
|
|
logo = QLabel("S")
|
|
logo.setObjectName("logo_label")
|
|
logo.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
sb_layout.addWidget(logo)
|
|
sb_layout.addSpacing(8)
|
|
|
|
icons = {
|
|
"dashboard": "⊞",
|
|
"chart": "📈",
|
|
"watchlist": "★",
|
|
"portfolio": "💼",
|
|
"ai": "✦",
|
|
"news": "📰",
|
|
"screener": "🔍",
|
|
}
|
|
labels = {
|
|
"dashboard": "Dashboard",
|
|
"chart": "Chart",
|
|
"watchlist": "Watchlist",
|
|
"portfolio": "Portfolio",
|
|
"ai": "AI",
|
|
"news": "News",
|
|
"screener": "Screener",
|
|
}
|
|
|
|
for page_id, icon in icons.items():
|
|
btn = SidebarButton(icon, labels[page_id])
|
|
btn.clicked.connect(lambda checked, pid=page_id: self._set_page(pid))
|
|
self._nav_buttons[page_id] = btn
|
|
sb_layout.addWidget(btn)
|
|
|
|
sb_layout.addStretch()
|
|
|
|
# Theme toggle
|
|
self._theme_btn = SidebarButton("☀", "Toggle Theme")
|
|
self._theme_btn.setCheckable(False)
|
|
self._theme_btn.clicked.connect(self._toggle_theme)
|
|
sb_layout.addWidget(self._theme_btn)
|
|
|
|
# Settings
|
|
settings_btn = SidebarButton("⚙", "Settings")
|
|
settings_btn.setCheckable(False)
|
|
settings_btn.clicked.connect(self._open_settings)
|
|
sb_layout.addWidget(settings_btn)
|
|
|
|
return sidebar
|
|
|
|
def _build_pages(self):
|
|
from ui.dashboard_widget import DashboardWidget
|
|
from ui.chart_widget import ChartWidget
|
|
from ui.watchlist_widget import WatchlistWidget
|
|
from ui.portfolio_widget import PortfolioWidget
|
|
from ui.ai_panel import AIPanelWidget
|
|
from ui.news_widget import NewsWidget
|
|
from ui.screener_widget import ScreenerWidget
|
|
|
|
self._dashboard = DashboardWidget()
|
|
self._chart = ChartWidget()
|
|
self._watchlist = WatchlistWidget()
|
|
self._portfolio = PortfolioWidget(self._config)
|
|
self._ai_panel = AIPanelWidget(self._config)
|
|
self._news = NewsWidget(self._config)
|
|
self._screener = ScreenerWidget()
|
|
|
|
pages = [
|
|
("dashboard", self._dashboard),
|
|
("chart", self._chart),
|
|
("watchlist", self._watchlist),
|
|
("portfolio", self._portfolio),
|
|
("ai", self._ai_panel),
|
|
("news", self._news),
|
|
("screener", self._screener),
|
|
]
|
|
self._page_indices: dict[str, int] = {}
|
|
for page_id, widget in pages:
|
|
idx = self._stack.addWidget(widget)
|
|
self._page_indices[page_id] = idx
|
|
|
|
# Wire cross-widget signals
|
|
self._dashboard.symbol_selected.connect(self._navigate_to_chart)
|
|
self._watchlist.symbol_selected.connect(self._navigate_to_chart)
|
|
self._portfolio.symbol_selected.connect(self._navigate_to_chart)
|
|
self._screener.symbol_selected.connect(self._navigate_to_chart)
|
|
self._screener.add_to_watchlist.connect(lambda s: self._watchlist.add_symbol_external(s))
|
|
self._chart.symbol_changed.connect(self._on_symbol_changed)
|
|
|
|
def _set_page(self, page_id: str):
|
|
for pid, btn in self._nav_buttons.items():
|
|
btn.setChecked(pid == page_id)
|
|
if page_id in self._page_indices:
|
|
self._stack.setCurrentIndex(self._page_indices[page_id])
|
|
self._status_page_label.setText(page_id.capitalize())
|
|
|
|
def _navigate_to_chart(self, symbol: str):
|
|
self._chart.load_symbol(symbol)
|
|
self._set_page("chart")
|
|
|
|
def _on_symbol_changed(self, symbol: str):
|
|
self._ai_panel.set_symbol(symbol)
|
|
self._news.set_symbol(symbol)
|
|
|
|
def _toggle_theme(self):
|
|
current = self._config.get_setting("theme", "dark")
|
|
new_theme = "light" if current == "dark" else "dark"
|
|
self._config.set_setting("theme", new_theme)
|
|
self._apply_theme(new_theme)
|
|
self._theme_btn.setText("☀" if new_theme == "dark" else "🌙")
|
|
|
|
def _apply_theme(self, theme: str):
|
|
base_dir = os.path.dirname(os.path.dirname(__file__))
|
|
path = os.path.join(base_dir, "assets", "styles", f"{theme}.qss")
|
|
if os.path.exists(path):
|
|
with open(path, "r") as f:
|
|
QApplication.instance().setStyleSheet(f.read())
|
|
|
|
def _open_settings(self):
|
|
from ui.settings_dialog import SettingsDialog
|
|
dlg = SettingsDialog(self._config, self)
|
|
dlg.exec()
|
|
|
|
def _start_alerts(self):
|
|
from core.alerts import AlertWorker
|
|
self._alert_worker = AlertWorker(interval_seconds=300)
|
|
self._alert_worker.alert_triggered.connect(self._on_alert_triggered)
|
|
self._alert_worker.start()
|
|
|
|
def _on_alert_triggered(self, symbol: str, alert_type: str, message: str):
|
|
self.statusBar().showMessage(f"⚡ Alert: {message}", 10000)
|
|
|
|
def _setup_status_bar(self):
|
|
sb = self.statusBar()
|
|
self._status_page_label = QLabel("Dashboard")
|
|
self._status_page_label.setObjectName("subtitle")
|
|
sb.addWidget(self._status_page_label)
|
|
|
|
self._market_status_label = QLabel()
|
|
self._market_status_label.setObjectName("subtitle")
|
|
sb.addPermanentWidget(self._market_status_label)
|
|
self._update_market_status()
|
|
|
|
timer = QTimer(self)
|
|
timer.timeout.connect(self._update_market_status)
|
|
timer.start(60_000)
|
|
|
|
def _update_market_status(self):
|
|
from datetime import datetime, timezone
|
|
import zoneinfo
|
|
try:
|
|
et = zoneinfo.ZoneInfo("America/New_York")
|
|
now_et = datetime.now(et)
|
|
weekday = now_et.weekday()
|
|
hour = now_et.hour
|
|
minute = now_et.minute
|
|
is_market_hours = (weekday < 5) and (9 * 60 + 30 <= hour * 60 + minute < 16 * 60)
|
|
status = "🟢 Market Open" if is_market_hours else "🔴 Market Closed"
|
|
self._market_status_label.setText(status)
|
|
except Exception:
|
|
self._market_status_label.setText("")
|
|
|
|
def closeEvent(self, event):
|
|
if self._alert_worker:
|
|
self._alert_worker.stop()
|
|
self._alert_worker.wait(2000)
|
|
super().closeEvent(event)
|