136 lines
4.4 KiB
Python
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()
|