Files
2026-05-26 11:04:27 -04:00

136 lines
4.4 KiB
Python

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