from __future__ import annotations from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QGridLayout, QFrame, QSizePolicy ) from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QThread from PyQt6.QtGui import QFont from utils.formatters import fmt_currency, fmt_percent, pnl_color INDICES = [ ("SPY", "S&P 500"), ("QQQ", "NASDAQ 100"), ("DIA", "Dow Jones"), ("BTC-USD", "Bitcoin"), ("ETH-USD", "Ethereum"), ] class QuoteCard(QFrame): clicked = pyqtSignal(str) def __init__(self, symbol: str, name: str, parent=None): super().__init__(parent) self.symbol = symbol self.setObjectName("card") self.setCursor(Qt.CursorShape.PointingHandCursor) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.setMinimumHeight(100) layout = QVBoxLayout(self) layout.setContentsMargins(12, 10, 12, 10) layout.setSpacing(2) top = QHBoxLayout() self._symbol_label = QLabel(symbol) self._symbol_label.setFont(QFont("Segoe UI", 10, QFont.Weight.Bold)) self._name_label = QLabel(name) self._name_label.setObjectName("subtitle") self._name_label.setAlignment(Qt.AlignmentFlag.AlignRight) top.addWidget(self._symbol_label) top.addWidget(self._name_label) layout.addLayout(top) self._price_label = QLabel("—") self._price_label.setFont(QFont("Segoe UI", 16, QFont.Weight.Bold)) layout.addWidget(self._price_label) self._change_label = QLabel("—") self._change_label.setObjectName("subtitle") layout.addWidget(self._change_label) def update_quote(self, data: dict): price = data.get("price", 0) change = data.get("change", 0) change_pct = data.get("change_pct", 0) self._price_label.setText(fmt_currency(price)) sign = "+" if change >= 0 else "" self._change_label.setText(f"{sign}{fmt_currency(change, 2)} {fmt_percent(change_pct, plus=True)}") color = pnl_color(change) self._change_label.setStyleSheet(f"color: {color};") def mousePressEvent(self, event): self.clicked.emit(self.symbol) super().mousePressEvent(event) class FetchWorker(QThread): quotes_ready = pyqtSignal(dict) def __init__(self, symbols: list[str]): super().__init__() self._symbols = symbols def run(self): from core.market_data import get_batch_quotes data = get_batch_quotes(self._symbols) self.quotes_ready.emit(data) class DashboardWidget(QWidget): symbol_selected = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self._cards: dict[str, QuoteCard] = {} self._worker: FetchWorker | None = None self._setup_ui() self._start_refresh() def _setup_ui(self): root = QVBoxLayout(self) root.setContentsMargins(20, 20, 20, 20) root.setSpacing(16) header = QLabel("Market Overview") header.setObjectName("title") root.addWidget(header) self._status_label = QLabel("Refreshing...") self._status_label.setObjectName("subtitle") root.addWidget(self._status_label) grid = QGridLayout() grid.setSpacing(12) for i, (symbol, name) in enumerate(INDICES): card = QuoteCard(symbol, name) card.clicked.connect(self.symbol_selected) self._cards[symbol] = card grid.addWidget(card, i // 3, i % 3) root.addLayout(grid) root.addStretch() def _start_refresh(self): self._timer = QTimer(self) self._timer.timeout.connect(self._fetch_quotes) self._timer.start(300_000) # 5 min self._fetch_quotes() def _fetch_quotes(self): if self._worker and self._worker.isRunning(): return self._status_label.setText("Refreshing...") self._worker = FetchWorker([s for s, _ in INDICES]) self._worker.quotes_ready.connect(self._on_quotes_ready) self._worker.start() def _on_quotes_ready(self, data: dict): from datetime import datetime for sym, quote in data.items(): if sym in self._cards: self._cards[sym].update_quote(quote) self._status_label.setText(f"Updated {datetime.now().strftime('%H:%M:%S')} (15-min delayed)") def force_refresh(self): self._fetch_quotes()