fix(HEL-164): 拒绝缓存不完整涨停快照
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -41,13 +41,16 @@ class DashboardMixin:
|
|||||||
raise TushareError(f"No daily data returned for {trade_date}")
|
raise TushareError(f"No daily data returned for {trade_date}")
|
||||||
|
|
||||||
notices: list[str] = []
|
notices: list[str] = []
|
||||||
|
limit_data_source = "official"
|
||||||
try:
|
try:
|
||||||
limit_rows = self._load_limit_lists(trade_date)
|
limit_rows = self._load_limit_lists(trade_date)
|
||||||
previous_limit_rows = self._load_limit_type(previous_trade_date, "U")
|
previous_limit_rows = self._load_limit_type(previous_trade_date, "U")
|
||||||
if not limit_rows:
|
if not limit_rows:
|
||||||
|
limit_data_source = "derived"
|
||||||
notices.append("涨跌停高级接口当日数据尚未更新,已使用日线数据推算。")
|
notices.append("涨跌停高级接口当日数据尚未更新,已使用日线数据推算。")
|
||||||
limit_rows = self._derive_limits(trade_date, daily)
|
limit_rows = self._derive_limits(trade_date, daily)
|
||||||
except TushareError as exc:
|
except TushareError as exc:
|
||||||
|
limit_data_source = "derived"
|
||||||
notices.append(f"涨跌停高级接口不可用,已使用日线数据推算:{exc}")
|
notices.append(f"涨跌停高级接口不可用,已使用日线数据推算:{exc}")
|
||||||
limit_rows = self._derive_limits(trade_date, daily)
|
limit_rows = self._derive_limits(trade_date, daily)
|
||||||
previous_daily = self._load_daily(previous_trade_date)
|
previous_daily = self._load_daily(previous_trade_date)
|
||||||
@@ -79,6 +82,7 @@ class DashboardMixin:
|
|||||||
"trade_date": _display_date(trade_date),
|
"trade_date": _display_date(trade_date),
|
||||||
"previous_trade_date": _display_date(previous_trade_date),
|
"previous_trade_date": _display_date(previous_trade_date),
|
||||||
"source": "tushare",
|
"source": "tushare",
|
||||||
|
"limit_data_source": limit_data_source,
|
||||||
"updated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
"updated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||||
"notice": ";".join(notices),
|
"notice": ";".join(notices),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -198,6 +198,11 @@ class MarketServiceMixin:
|
|||||||
raise TushareError("公共行情尚未配置")
|
raise TushareError("公共行情尚未配置")
|
||||||
dashboard = self._tushare_client().dashboard(normalized_date)
|
dashboard = self._tushare_client().dashboard(normalized_date)
|
||||||
|
|
||||||
|
if (dashboard.get("meta") or {}).get("limit_data_source") == "derived":
|
||||||
|
raise TushareError(
|
||||||
|
str((dashboard.get("meta") or {}).get("notice") or "官方涨跌停数据尚未返回")
|
||||||
|
)
|
||||||
|
|
||||||
dashboard["meta"]["source"] = source
|
dashboard["meta"]["source"] = source
|
||||||
dashboard["meta"]["requested_date"] = self._display_compact_date(normalized_date)
|
dashboard["meta"]["requested_date"] = self._display_compact_date(normalized_date)
|
||||||
dashboard = self._enrich_dashboard_sentiment(dashboard, normalized_date)
|
dashboard = self._enrich_dashboard_sentiment(dashboard, normalized_date)
|
||||||
|
|||||||
+11
-1
@@ -7,6 +7,16 @@ from datetime import date
|
|||||||
from backend.bootstrap.config import normalize_date
|
from backend.bootstrap.config import normalize_date
|
||||||
|
|
||||||
|
|
||||||
|
def _verified_dashboard_result(dashboard: dict[str, object]) -> dict[str, object]:
|
||||||
|
meta = dashboard.get("meta") or {}
|
||||||
|
if isinstance(meta, dict) and meta.get("carried_forward"):
|
||||||
|
return {
|
||||||
|
"status": "failed",
|
||||||
|
"error": str(meta.get("notice") or "未获取到所选日期的最新行情"),
|
||||||
|
}
|
||||||
|
return dashboard
|
||||||
|
|
||||||
|
|
||||||
class JobServiceMixin:
|
class JobServiceMixin:
|
||||||
def start_background_jobs(self) -> threading.Thread:
|
def start_background_jobs(self) -> threading.Thread:
|
||||||
return self.jobs.start_scheduler(
|
return self.jobs.start_scheduler(
|
||||||
@@ -26,7 +36,7 @@ class JobServiceMixin:
|
|||||||
started = self.jobs.submit(
|
started = self.jobs.submit(
|
||||||
"market.refresh",
|
"market.refresh",
|
||||||
key,
|
key,
|
||||||
lambda: self.sync_dashboard(normalized),
|
lambda: _verified_dashboard_result(self.sync_dashboard(normalized)),
|
||||||
{"trade_date": normalized, "trigger": "administrator"},
|
{"trade_date": normalized, "trigger": "administrator"},
|
||||||
)
|
)
|
||||||
return {"started": started, "job_key": key if started else ""}
|
return {"started": started, "job_key": key if started else ""}
|
||||||
|
|||||||
@@ -486,8 +486,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/data/providers/tushare_dashboard.py",
|
"path": "backend/data/providers/tushare_dashboard.py",
|
||||||
"bytes": 28051,
|
"bytes": 28234,
|
||||||
"lines": 644
|
"lines": 648
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/data/providers/tushare_industries.py",
|
"path": "backend/data/providers/tushare_industries.py",
|
||||||
@@ -769,6 +769,11 @@
|
|||||||
"bytes": 2299,
|
"bytes": 2299,
|
||||||
"lines": 57
|
"lines": 57
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "backend/jobs/service.py",
|
||||||
|
"bytes": 2219,
|
||||||
|
"lines": 60
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/screener/regime.py",
|
"path": "backend/features/screener/regime.py",
|
||||||
"bytes": 2202,
|
"bytes": 2202,
|
||||||
@@ -799,11 +804,6 @@
|
|||||||
"bytes": 1919,
|
"bytes": 1919,
|
||||||
"lines": 45
|
"lines": 45
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "backend/jobs/service.py",
|
|
||||||
"bytes": 1833,
|
|
||||||
"lines": 50
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "backend/features/system/routes.py",
|
"path": "backend/features/system/routes.py",
|
||||||
"bytes": 1791,
|
"bytes": 1791,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from backend.jobs.service import _verified_dashboard_result
|
||||||
|
|
||||||
|
|
||||||
|
class AdminRefreshStatusTests(unittest.TestCase):
|
||||||
|
def test_carried_snapshot_is_reported_as_failed_job(self):
|
||||||
|
result = _verified_dashboard_result(
|
||||||
|
{"meta": {"carried_forward": True, "notice": "官方涨跌停数据尚未返回"}}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result["status"], "failed")
|
||||||
|
self.assertEqual(result["error"], "官方涨跌停数据尚未返回")
|
||||||
|
|
||||||
|
def test_current_snapshot_is_reported_as_successful_job(self):
|
||||||
|
dashboard = {"meta": {"trade_date": "2026-08-28", "carried_forward": False}}
|
||||||
|
|
||||||
|
self.assertIs(_verified_dashboard_result(dashboard), dashboard)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -111,6 +111,25 @@ class RealtimeDashboardTests(unittest.TestCase):
|
|||||||
self.assertEqual(quote["amount_billion"], 3.0)
|
self.assertEqual(quote["amount_billion"], 3.0)
|
||||||
self.assertAlmostEqual(quote["turnover_rate"], 0.01)
|
self.assertAlmostEqual(quote["turnover_rate"], 0.01)
|
||||||
|
|
||||||
|
def test_close_dashboard_marks_official_limit_data(self):
|
||||||
|
dashboard = self.client.dashboard("20260720")
|
||||||
|
|
||||||
|
self.assertEqual(dashboard["meta"]["limit_data_source"], "official")
|
||||||
|
|
||||||
|
def test_close_dashboard_marks_derived_limit_data_as_incomplete(self):
|
||||||
|
original_query = self.client.query
|
||||||
|
|
||||||
|
def query(api_name, params=None, fields=""):
|
||||||
|
if api_name == "limit_list_d":
|
||||||
|
return []
|
||||||
|
return original_query(api_name, params, fields)
|
||||||
|
|
||||||
|
self.client.query = query
|
||||||
|
dashboard = self.client.dashboard("20260720")
|
||||||
|
|
||||||
|
self.assertEqual(dashboard["meta"]["limit_data_source"], "derived")
|
||||||
|
self.assertIn("日线数据推算", dashboard["meta"]["notice"])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user