Phase 1: initial codes
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user