fix: refresh quotes without blocking UI

This commit is contained in:
王鹏
2026-07-09 10:01:15 +08:00
parent 1e8858ac04
commit 659846fe18
3 changed files with 114 additions and 4 deletions

View File

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

View File

@@ -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):

View File

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