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