fix: refresh quotes without blocking UI
This commit is contained in:
@@ -94,10 +94,19 @@ class TradingService:
|
||||
|
||||
def refresh_quotes(self) -> dict[str, QuoteSnapshot]:
|
||||
instruments = self.repository.list_instruments()
|
||||
quotes = self.quote_provider.fetch_quotes(instruments)
|
||||
self._quote_cache = quotes
|
||||
quotes = self.fetch_quotes_for_instruments(instruments)
|
||||
self.apply_quote_snapshots(quotes)
|
||||
return quotes
|
||||
|
||||
def fetch_quotes_for_instruments(
|
||||
self,
|
||||
instruments: list[Instrument],
|
||||
) -> dict[str, QuoteSnapshot]:
|
||||
return self.quote_provider.fetch_quotes(instruments)
|
||||
|
||||
def apply_quote_snapshots(self, quotes: dict[str, QuoteSnapshot]) -> None:
|
||||
self._quote_cache = quotes
|
||||
|
||||
def get_default_strategy_template(self) -> StrategyTemplate:
|
||||
self.ensure_defaults()
|
||||
template = self.repository.get_default_strategy_template()
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtCore import QObject, Qt, QThread, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
@@ -31,6 +31,24 @@ from grid_trading.ui.dialogs import AccountDialog, InstrumentDialog, StrategyTem
|
||||
from grid_trading.ui.formatters import format_money, format_percent, format_price, format_quantity
|
||||
|
||||
|
||||
class QuoteRefreshWorker(QObject):
|
||||
finished = Signal(object)
|
||||
failed = Signal(str)
|
||||
|
||||
def __init__(self, service: TradingService, instruments):
|
||||
super().__init__()
|
||||
self._service = service
|
||||
self._instruments = instruments
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
quotes = self._service.fetch_quotes_for_instruments(self._instruments)
|
||||
except Exception as exc:
|
||||
self.failed.emit(str(exc))
|
||||
return
|
||||
self.finished.emit(quotes)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
HOLDING_COLUMNS = [
|
||||
"代码",
|
||||
@@ -56,6 +74,9 @@ class MainWindow(QMainWindow):
|
||||
self._positions: list[PositionSummary] = []
|
||||
self._selected_instrument_id: int | None = None
|
||||
self._trade_ids_by_row: dict[int, int] = {}
|
||||
self._quote_thread: QThread | None = None
|
||||
self._quote_worker: QuoteRefreshWorker | None = None
|
||||
self.refresh_quotes_button: QPushButton | None = None
|
||||
|
||||
self.setWindowTitle("Grid Trading Manager")
|
||||
self.resize(1280, 780)
|
||||
@@ -168,6 +189,8 @@ class MainWindow(QMainWindow):
|
||||
for text, handler in buttons:
|
||||
button = QPushButton(text)
|
||||
button.clicked.connect(handler)
|
||||
if text == "刷新行情":
|
||||
self.refresh_quotes_button = button
|
||||
layout.addWidget(button)
|
||||
layout.addStretch()
|
||||
return layout
|
||||
@@ -188,13 +211,51 @@ class MainWindow(QMainWindow):
|
||||
self._refresh_details()
|
||||
|
||||
def _refresh_quotes(self) -> None:
|
||||
if self._quote_thread is not None and self._quote_thread.isRunning():
|
||||
return
|
||||
try:
|
||||
self.service.refresh_quotes()
|
||||
instruments = self.service.list_instruments()
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "行情刷新失败", str(exc))
|
||||
return
|
||||
if not instruments:
|
||||
QMessageBox.information(self, "行情刷新", "请先添加股票或 ETF 标的。")
|
||||
return
|
||||
self._set_quote_refreshing(True)
|
||||
self._quote_thread = QThread(self)
|
||||
self._quote_worker = QuoteRefreshWorker(self.service, instruments)
|
||||
self._quote_worker.moveToThread(self._quote_thread)
|
||||
self._quote_thread.started.connect(self._quote_worker.run)
|
||||
self._quote_worker.finished.connect(self._on_quote_refresh_finished)
|
||||
self._quote_worker.failed.connect(self._on_quote_refresh_failed)
|
||||
self._quote_worker.finished.connect(self._quote_thread.quit)
|
||||
self._quote_worker.failed.connect(self._quote_thread.quit)
|
||||
self._quote_thread.finished.connect(self._quote_worker.deleteLater)
|
||||
self._quote_thread.finished.connect(self._on_quote_thread_finished)
|
||||
self._quote_thread.start()
|
||||
|
||||
def _on_quote_refresh_finished(self, quotes: dict) -> None:
|
||||
try:
|
||||
self.service.apply_quote_snapshots(quotes)
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "行情刷新失败", str(exc))
|
||||
return
|
||||
self.refresh_all()
|
||||
|
||||
def _on_quote_refresh_failed(self, message: str) -> None:
|
||||
QMessageBox.warning(self, "行情刷新失败", message)
|
||||
|
||||
def _on_quote_thread_finished(self) -> None:
|
||||
self._set_quote_refreshing(False)
|
||||
self._quote_thread = None
|
||||
self._quote_worker = None
|
||||
|
||||
def _set_quote_refreshing(self, refreshing: bool) -> None:
|
||||
if self.refresh_quotes_button is None:
|
||||
return
|
||||
self.refresh_quotes_button.setEnabled(not refreshing)
|
||||
self.refresh_quotes_button.setText("刷新中..." if refreshing else "刷新行情")
|
||||
|
||||
def _fill_holdings_table(self) -> None:
|
||||
self.holdings_table.setRowCount(len(self._positions))
|
||||
for row, position in enumerate(self._positions):
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
from decimal import Decimal
|
||||
import time
|
||||
|
||||
from grid_trading.domain.models import Instrument
|
||||
|
||||
|
||||
def test_formatters_render_money_percent_and_empty_values():
|
||||
@@ -30,3 +33,40 @@ def test_main_window_can_be_constructed_offscreen(tmp_path, monkeypatch):
|
||||
window.close()
|
||||
service.close()
|
||||
app.processEvents()
|
||||
|
||||
|
||||
def test_quote_refresh_does_not_block_main_window(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from grid_trading.services.trading_service import TradingService
|
||||
from grid_trading.ui.main_window import MainWindow
|
||||
|
||||
class SlowQuoteProvider:
|
||||
def fetch_quotes(self, instruments):
|
||||
time.sleep(0.3)
|
||||
return {}
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
service = TradingService(tmp_path / "grid.db", quote_provider=SlowQuoteProvider())
|
||||
service.ensure_defaults()
|
||||
service.add_instrument(Instrument(id=None, code="000001", name="Ping An Bank"))
|
||||
window = MainWindow(service)
|
||||
|
||||
started_at = time.perf_counter()
|
||||
window._refresh_quotes()
|
||||
elapsed = time.perf_counter() - started_at
|
||||
|
||||
assert elapsed < 0.15
|
||||
|
||||
deadline = time.perf_counter() + 2
|
||||
while getattr(window, "_quote_thread", None) is not None and time.perf_counter() < deadline:
|
||||
app.processEvents()
|
||||
time.sleep(0.01)
|
||||
|
||||
assert getattr(window, "_quote_thread", None) is None
|
||||
|
||||
window.close()
|
||||
service.close()
|
||||
app.processEvents()
|
||||
|
||||
Reference in New Issue
Block a user