fix(HEL-494): 数据中枢独占调度,主网站不再回退旧接口

主网站只向中枢要业务数据;来源选择、切源、补数全部在中枢内部完成,失败不再走东财/腾讯/Tushare 保底。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
总工
2026-09-08 21:43:31 +08:00
co-authored by Cursor multica-agent
parent ef13d6feb5
commit 0b8419abca
23 changed files with 1159 additions and 368 deletions
+4 -5
View File
@@ -202,7 +202,7 @@ class DatahubChartFallbackTests(unittest.TestCase):
self.assertEqual(hub.calls, ["601318"])
self.assertEqual(fallback.requests, [])
def test_datahub_timeout_or_empty_falls_back_to_eastmoney(self):
def test_datahub_timeout_or_empty_does_not_use_old_channel(self):
fallback = LookbackChartClient()
for hub in (
FakeHub(chart=None),
@@ -213,10 +213,9 @@ class DatahubChartFallbackTests(unittest.TestCase):
EastmoneyChartClient._cache.clear()
fallback.requests.clear()
client = MarketChartClient(IfindHttpClient(), fallback, hub)
payload = client.stock_intraday("000001")
self.assertEqual(payload["trade_date"], "2026-09-07")
self.assertGreaterEqual(len(payload["points"]), 1)
self.assertTrue(fallback.requests)
with self.assertRaises(ChartDataError):
client.stock_intraday("000001")
self.assertEqual(fallback.requests, [])
def test_datahub_daily_skips_ifind(self):
hub = FakeHub(
+51 -59
View File
@@ -13,6 +13,7 @@ from backend.data.datahub.compare import compare_rows
from backend.data.datahub.errors import DatahubError
from backend.data.datahub.native import to_canonical_row, to_native_row
from backend.data.datahub.route_state import LEDGER
from backend.data.providers.tushare_transport import TushareError
from backend.data.datahub.settings import DATASETS, DatahubSettings, DatasetFlags
ROOT = Path(__file__).resolve().parents[1]
@@ -68,9 +69,16 @@ class FakeClient(DatahubClient):
self.calls: list[tuple[str, dict[str, Any]]] = []
def get(self, path: str, params: dict[str, Any] | None = None) -> DatahubResponse:
return self._record(path, params)
def post(self, path: str, body: dict[str, Any] | None = None) -> DatahubResponse:
return self._record(path, body)
def _record(self, path: str, payload: dict[str, Any] | None) -> DatahubResponse:
self.paths.append(path)
self.calls.append((path, {key: value for key, value in (params or {}).items()}))
if TOKEN in json.dumps(params or {}) or TOKEN in path:
self.calls.append((path, {key: value for key, value in (payload or {}).items()}))
packed = json.dumps(payload or {})
if TOKEN in packed or TOKEN in path:
raise AssertionError("token leaked into url")
if self.error:
raise self.error
@@ -131,16 +139,22 @@ class DatahubBridgeTests(unittest.TestCase):
self.assertEqual(legacy.calls, [])
self.assertEqual(client.paths, ["/v1/bars/daily"])
calendar_legacy = FakeLegacy([{"cal_date": "20240902", "is_open": 1}])
calendar_client = FakeClient(error=DatahubError("INTERNAL", "nope"))
calendar_client = FakeClient(
response=DatahubResponse(
data=[{"cal_date": "20240902", "is_open": 1, "pretrade_date": "20240830"}],
meta={"source": "datahub", "stale": False, "staleness_seconds": 0},
)
)
calendar_wrapped = DatahubAwareTushareClient(
calendar_legacy,
DatahubBridge(flags(daily=(True, False)), calendar_client),
)
calendar = calendar_wrapped.query("trade_cal", {"start_date": "20240902", "end_date": "20240902"}, "")
self.assertEqual(calendar[0]["is_open"], 1)
self.assertEqual(calendar_client.paths, [])
self.assertEqual(calendar_legacy.calls, [])
self.assertEqual(calendar_client.paths, ["/v1/query"])
def test_fallback_on_down_401_timeout_empty_unpublished_stale_and_incomplete(self) -> None:
def test_hub_failure_does_not_call_website_legacy(self) -> None:
cases = [
DatahubError("UNAVAILABLE", "down"),
DatahubError("UNAUTHORIZED", "401"),
@@ -152,34 +166,21 @@ class DatahubBridgeTests(unittest.TestCase):
]
for error in cases:
with self.subTest(error=error.code):
if error.code == "EMPTY":
client = FakeClient(response=DatahubResponse(data=[], meta={"stale": False, "staleness_seconds": 0}))
elif error.code == "STALE":
client = FakeClient(response=DatahubResponse(
data=[dict(HUB_DAILY)],
meta={"stale": True, "staleness_seconds": 999999},
))
elif error.code == "INCOMPLETE":
client = FakeClient(response=DatahubResponse(
data=[dict(HUB_DAILY)],
meta={
"stale": False,
"staleness_seconds": 0,
"incomplete": True,
"coverage": {"complete": False, "missing_count": 80},
},
))
else:
client = FakeClient(error=error)
client = FakeClient(error=error)
legacy = FakeLegacy([LEGACY_DAILY])
wrapped = DatahubAwareTushareClient(legacy, DatahubBridge(flags(daily=(True, False)), client))
rows = wrapped.query("daily", {"trade_date": "20240902"}, "ts_code,amount")
self.assertEqual(rows[0]["amount"], 2000.0)
self.assertEqual(len(legacy.calls), 1)
with self.assertRaises(TushareError):
wrapped.query("daily", {"trade_date": "20240902"}, "ts_code,amount")
self.assertEqual(legacy.calls, [])
def test_shadow_compares_without_replacing_and_survives_hub_failure(self) -> None:
def test_shadow_mode_no_longer_calls_website_tushare(self) -> None:
reports: list[dict[str, Any]] = []
client = FakeClient()
client = FakeClient(
response=DatahubResponse(
data=[dict(LEGACY_DAILY)],
meta={"source": "tushare", "stale": False, "staleness_seconds": 0, "row_shape": "tushare"},
)
)
legacy = FakeLegacy([LEGACY_DAILY])
wrapped = DatahubAwareTushareClient(
legacy,
@@ -187,21 +188,19 @@ class DatahubBridgeTests(unittest.TestCase):
)
rows = wrapped.query("daily", {"trade_date": "20240902"}, "ts_code,amount,vol")
self.assertEqual(rows[0]["amount"], 2000.0)
self.assertEqual(len(legacy.calls), 1)
self.assertEqual(reports[0]["equal"], True)
self.assertEqual(reports[0]["matched"], 1)
self.assertEqual(legacy.calls, [])
self.assertEqual(client.paths, ["/v1/query"])
failed = FakeClient(error=DatahubError("UNAVAILABLE", TOKEN))
fail_reports: list[dict[str, Any]] = []
fail_legacy = FakeLegacy([LEGACY_DAILY])
fail_wrapped = DatahubAwareTushareClient(
fail_legacy,
DatahubBridge(flags(daily=(False, True)), failed, shadow_sink=fail_reports.append),
DatahubBridge(flags(daily=(False, True)), failed, shadow_sink=reports.append),
)
again = fail_wrapped.query("daily", {"trade_date": "20240902"}, "amount")
self.assertEqual(again[0]["amount"], 2000.0)
self.assertTrue(fail_reports[0]["hub_error"])
self.assertNotIn(TOKEN, json.dumps(fail_reports[0]))
with self.assertRaises(TushareError):
fail_wrapped.query("daily", {"trade_date": "20240902"}, "amount")
self.assertEqual(fail_legacy.calls, [])
self.assertNotIn(TOKEN, str(failed.calls))
def test_compare_classifies_unit_conversion_missing_row_and_value_diff(self) -> None:
equal = compare_rows("daily", [LEGACY_DAILY], [HUB_DAILY], {"stale": False, "staleness_seconds": 0})
@@ -288,13 +287,12 @@ class DatahubBridgeTests(unittest.TestCase):
)
wrapped = DatahubAwareTushareClient(
FakeLegacy([legacy_close_only]),
DatahubBridge(flags(daily=(False, True)), client, shadow_sink=reports.append),
DatahubBridge(flags(daily=(True, False)), client, shadow_sink=reports.append),
)
rows = wrapped.query("daily", {"trade_date": "20240902"}, "ts_code,trade_date,close,vol,amount")
self.assertEqual(rows[0]["close"], 10.20)
self.assertEqual(rows[0]["vol"], 1000.0)
self.assertTrue(reports[0]["equal"])
self.assertEqual(reports[0]["matched"], 1)
self.assertEqual(client.paths, ["/v1/bars/daily"])
def test_native_roundtrip_matches_known_scales(self) -> None:
native = to_native_row("daily", HUB_DAILY)
@@ -347,21 +345,17 @@ class DatahubBridgeTests(unittest.TestCase):
self.assertIn('"daily"', source)
self.assertIn("start_date", source)
self.assertIn("end_date", source)
client = FakeClient(
response=DatahubResponse(
data=[dict(HUB_DAILY)],
meta={"stale": False, "staleness_seconds": 0, "incomplete": True, "coverage": {"complete": False, "missing_count": 89}},
)
)
client = FakeClient(error=DatahubError("INCOMPLETE", "truncated"))
legacy = FakeLegacy([LEGACY_DAILY])
wrapped = DatahubAwareTushareClient(legacy, DatahubBridge(flags(daily=(True, False)), client))
rows = wrapped.query(
"daily",
{"ts_code": "600000.SH", "start_date": "20240301", "end_date": "20240902"},
"ts_code,amount",
)
self.assertEqual(rows[0]["amount"], 2000.0)
self.assertEqual(len(legacy.calls), 1)
with self.assertRaises(TushareError):
wrapped.query(
"daily",
{"ts_code": "600000.SH", "start_date": "20240301", "end_date": "20240902"},
"ts_code,amount",
)
self.assertEqual(legacy.calls, [])
self.assertIn("/v1/query", client.paths)
def test_try_intraday_respects_switch_and_falls_back_on_bad_payload(self) -> None:
closed = DatahubBridge(flags(), FakeClient(error=DatahubError("INTERNAL", "should not run")))
@@ -462,17 +456,15 @@ class DatahubBridgeTests(unittest.TestCase):
FakeClient(error=DatahubError("UNAVAILABLE", "down")),
)
self.assertIsNone(failed.try_market_quotes("20240902"))
failed.record_legacy("quotes", "tencent_qt", "down")
snap = next(item for item in LEDGER.snapshot() if item["dataset"] == "quotes")
self.assertEqual(snap["route"], "legacy")
self.assertEqual(snap["source"], "tencent_qt")
self.assertIn("备用", "备用")
self.assertEqual(snap["route"], "datahub")
self.assertEqual(snap["source"], "unavailable")
gateway = build_data_gateway({}, datahub_settings=flags(quotes=(True, False)))
status = gateway.datahub_status()
self.assertEqual(status["enabled_reads"], 1)
self.assertEqual(status["total_reads"], len(DATASETS))
self.assertGreaterEqual(status["fallback_count"], 1)
self.assertEqual(status["fallback_count"], 0)
def test_try_daily_chart_converts_hub_bars(self) -> None:
rows = [
+13 -17
View File
@@ -87,11 +87,10 @@ class ShenwanRealtimeSourceTests(unittest.TestCase):
with self.assertRaisesRegex(TushareError, "rt_sw_k is disabled"):
client.query("rt_sw_k", {"ts_code": "801074.SI"})
def test_outer_realtime_uses_eastmoney_shenwan_not_rt_sw_k(self) -> None:
def test_outer_realtime_uses_hub_sector_quote_not_rt_sw_k(self) -> None:
client = TushareClient(token="demo")
client.query = MagicMock(side_effect=AssertionError("should not call tushare"))
client.realtime_aggregator = MagicMock()
client.realtime_aggregator.eastmoney_shenwan_quote.return_value = {
client.try_sector_quote = MagicMock(return_value={
"code": "801074.SI",
"name": "工业金属",
"close": 1234.5,
@@ -101,7 +100,7 @@ class ShenwanRealtimeSourceTests(unittest.TestCase):
"quote_date": "20260908",
"quote_time": "2026-09-08T14:50:00+08:00",
"source": "eastmoney_sw",
}
})
row, source, error = client._sw_outer_realtime("801074.SI", "工业金属", "20260908")
self.assertEqual(source, "eastmoney_sw")
self.assertEqual(error, "")
@@ -216,23 +215,21 @@ class MemberQuoteCoverageTests(unittest.TestCase):
self.assertEqual(source, "datahub")
client.try_quotes.assert_not_called()
def test_eastmoney_failure_uses_tencent_member_quotes(self) -> None:
def test_hub_named_quotes_cover_members_when_market_missing(self) -> None:
client = TushareClient(token="demo")
wanted = ["000737.SZ", "000630.SZ"]
client.try_market_quotes = MagicMock(return_value=None)
client.try_quotes = MagicMock(return_value=None)
aggregator = MagicMock()
aggregator.eastmoney_stock_quotes.side_effect = RuntimeError("HTTP 503")
aggregator.tencent_stock_quotes.return_value = [
client.try_quotes = MagicMock(return_value=[
{"ts_code": "000737.SZ", "close": 12.3, "pre_close": 11.2},
{"ts_code": "000630.SZ", "close": 4.5, "pre_close": 4.4},
]
client.realtime_aggregator = aggregator
client._free_realtime_quotes = MagicMock(side_effect=AssertionError("tencent already won"))
])
client.realtime_aggregator = MagicMock()
rows, source = client._load_member_realtime_quotes(wanted, "20260908")
self.assertEqual(len(rows), 2)
self.assertEqual(source, "tencent_qt")
aggregator.tencent_stock_quotes.assert_called_once()
self.assertEqual(source, "datahub")
client.try_quotes.assert_called()
client.realtime_aggregator.eastmoney_stock_quotes.assert_not_called()
client.realtime_aggregator.tencent_stock_quotes.assert_not_called()
def test_delayed_hub_quotes_are_kept_not_zeroed(self) -> None:
client = TushareClient(token="demo")
@@ -323,8 +320,7 @@ class MemberQuoteCoverageTests(unittest.TestCase):
client._sw_realtime_sector_snapshot = MagicMock(
side_effect=AssertionError("daily inner should be kept")
)
client.realtime_aggregator = MagicMock()
client.realtime_aggregator.eastmoney_shenwan_quote.return_value = {
client.try_sector_quote = MagicMock(return_value={
"code": "801074.SI",
"name": "工业金属",
"change": 1.5,
@@ -332,7 +328,7 @@ class MemberQuoteCoverageTests(unittest.TestCase):
"quote_date": "20260908",
"quote_time": "2026-09-08T15:00:00+08:00",
"source": "eastmoney_sw",
}
})
snapshot = client.sw_sector_snapshot(
"000737.SZ", "20260908", allow_realtime_close=True
)
+106
View File
@@ -0,0 +1,106 @@
from __future__ import annotations
import ast
import unittest
from pathlib import Path
from unittest.mock import patch
from backend.data.datahub.bridge import DatahubAwareTushareClient, DatahubBridge
from backend.data.datahub.client import DatahubClient
from backend.data.datahub.settings import DATASETS, DatahubSettings, DatasetFlags
from backend.data.providers.tushare_transport import TushareError
from tests.test_datahub_bridge import FakeClient, FakeLegacy, flags
ROOT = Path(__file__).resolve().parents[1]
BLOCKED_HOSTS = (
"api.tushare.pro",
"push2.eastmoney.com",
"push2delay.eastmoney.com",
"push2his.eastmoney.com",
"push2ex.eastmoney.com",
"qt.gtimg.cn",
"hq.sinajs.cn",
"10jqka.com.cn",
"xuangubao.cn",
)
class HubExclusiveWebsiteTests(unittest.TestCase):
def test_query_never_calls_website_tushare_transport(self) -> None:
client = FakeClient()
legacy = FakeLegacy(TushareError("website tushare must stay dark"))
wrapped = DatahubAwareTushareClient(
legacy,
DatahubBridge(flags(daily=(True, False)), client),
)
rows = wrapped.query("daily", {"trade_date": "20240902"}, "ts_code,amount")
self.assertEqual(rows[0]["amount"], 2000.0)
self.assertEqual(legacy.calls, [])
def test_blocked_external_hosts_still_read_hub(self) -> None:
settings = DatahubSettings(
base_url="http://127.0.0.1:8766",
token="hub-token",
datasets={name: DatasetFlags(name, read=True) for name in DATASETS},
)
def blocked_urlopen(request, timeout=None):
url = str(getattr(request, "full_url", None) or request)
if any(host in url for host in BLOCKED_HOSTS):
raise AssertionError(f"website opened blocked host: {url}")
if "127.0.0.1:8766" in url or "v1/bars/daily" in url:
class _Resp:
status = 200
def read(self):
return (
b'{"schema_version":1,"data":[{"ts_code":"600000.SH","trade_date":"20240902",'
b'"close":10.2,"volume":100000,"amount":2000000}],'
b'"meta":{"stale":false,"staleness_seconds":0,"source":"datahub"}}'
)
def __enter__(self):
return self
def __exit__(self, *args):
return False
return _Resp()
raise AssertionError(f"unexpected url: {url}")
hub_client = DatahubClient(settings, urlopen=blocked_urlopen)
legacy = FakeLegacy(TushareError("blocked"))
wrapped = DatahubAwareTushareClient(legacy, DatahubBridge(settings, hub_client))
with patch("urllib.request.urlopen", blocked_urlopen):
rows = wrapped.query("daily", {"trade_date": "20240902"}, "ts_code,close,amount")
self.assertEqual(rows[0]["close"], 10.2)
self.assertEqual(rows[0]["amount"], 2000.0)
self.assertEqual(legacy.calls, [])
def test_website_runtime_does_not_call_blocked_hosts_from_gateway(self) -> None:
gateway_src = (ROOT / "backend" / "data" / "gateway.py").read_text(encoding="utf-8")
self.assertIn("legacy.realtime_aggregator = None", gateway_src)
self.assertIn("DatahubAwareTushareClient", gateway_src)
def test_bridge_query_has_no_legacy_call(self) -> None:
source = (ROOT / "backend" / "data" / "datahub" / "bridge.py").read_text(encoding="utf-8")
tree = ast.parse(source)
query_fn = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "DatahubBridge"
for item in node.body
if isinstance(item, ast.FunctionDef) and item.name == "query"
)
called = [
ast.unparse(item.func) if hasattr(ast, "unparse") else ""
for item in ast.walk(query_fn)
if isinstance(item, ast.Call)
]
self.assertTrue(any("query_api" in text for text in called))
self.assertFalse(any("legacy_query" in text for text in called))
if __name__ == "__main__":
unittest.main()
+53 -2
View File
@@ -12,6 +12,20 @@ from backend.features.market.insights import MarketInsightsService
from server import DashboardService
class _FakeDailyHub:
def __init__(self, rows: list) -> None:
self.rows = rows
def try_daily_chart(self, code, end_date, limit, dataset="daily"):
return list(self.rows)
def try_quotes(self, codes):
return None
def try_index_quotes(self):
return None
class FakeIfind:
configured = True
@@ -128,13 +142,50 @@ class IfindFeatureTests(unittest.TestCase):
self.assertEqual(database.list_wencai_saved_queries(second["id"]), [])
def test_ifind_daily_chart_normalizes_change(self):
client = MarketChartClient(FakeIfind(), EastmoneyChartClient())
hub = _FakeDailyHub(
[
{
"trade_date": "2026-07-27",
"open": 10,
"high": 10.5,
"low": 9.8,
"close": 10.2,
"volume": 100,
"amount_billion": 0.01,
"change": 0,
},
{
"trade_date": "2026-07-28",
"open": 10.2,
"high": 10.8,
"low": 10.1,
"close": 10.5,
"volume": 120,
"amount_billion": 0.012,
"change": 2.9412,
},
]
)
client = MarketChartClient(FakeIfind(), EastmoneyChartClient(), hub)
rows = client.stock_daily("000001", "20260728")
self.assertEqual(rows[-1]["trade_date"], "2026-07-28")
self.assertAlmostEqual(rows[-1]["change"], 2.9412, places=4)
def test_ifind_daily_chart_keeps_last_traded_bar_before_market_open(self):
client = MarketChartClient(FakeIfindStalePreopen(), EastmoneyChartClient())
hub = _FakeDailyHub(
[
{
"trade_date": "2026-07-28",
"open": 10.2,
"high": 10.8,
"low": 10.1,
"close": 10.5,
"volume": 120,
"amount_billion": 0.012,
}
]
)
client = MarketChartClient(FakeIfindStalePreopen(), EastmoneyChartClient(), hub)
with patch("backend.features.market.charts.datetime", FixedPreopenDatetime):
rows = client.stock_daily("000001", "20260729")
+20 -26
View File
@@ -256,7 +256,7 @@ class RealtimeDashboardTests(unittest.TestCase):
self.assertEqual(dashboard["meta"]["quote_count"], 3)
self.assertEqual(dashboard["overview"]["limit_up_count"], 0)
def test_rt_k_permission_error_falls_back_to_free_quotes(self):
def test_hub_quotes_used_when_rt_k_denied(self):
original_query = self.client.query
def query(api_name, params=None, fields=""):
@@ -265,21 +265,20 @@ class RealtimeDashboardTests(unittest.TestCase):
return original_query(api_name, params, fields)
self.client.query = query
self.client.realtime_aggregator = FakeFreeAggregator()
self.client.try_market_quotes = lambda trade_date: list(FREE_QUOTES)
TushareClient._realtime_reference_cache.clear()
dashboard = self.client._realtime_dashboard("20260720", "20260720", "20260717")
self.assertTrue(dashboard["meta"]["realtime"])
self.assertEqual(dashboard["meta"]["quote_source"], "eastmoney_clist")
self.assertEqual(dashboard["meta"]["quote_source"], "datahub")
self.assertEqual(dashboard["meta"]["trade_date"], "2026-07-20")
self.assertEqual(dashboard["meta"]["quote_count"], 3)
self.assertEqual(dashboard["overview"]["limit_up_count"], 1)
self.assertEqual(dashboard["overview"]["limit_down_count"], 1)
self.assertEqual(dashboard["overview"]["amount_billion"], 6.0)
self.assertIn("东财免费实时", dashboard["meta"]["notice"])
self.assertEqual(dashboard["meta"]["indices"][0]["price"], 3800.12)
self.assertIn("数据中枢", dashboard["meta"]["notice"])
def test_rt_k_empty_result_falls_back_to_free_quotes(self):
def test_hub_quotes_used_when_rt_k_empty(self):
original_query = self.client.query
def query(api_name, params=None, fields=""):
@@ -288,29 +287,27 @@ class RealtimeDashboardTests(unittest.TestCase):
return original_query(api_name, params, fields)
self.client.query = query
self.client.realtime_aggregator = FakeFreeAggregator()
self.client.try_market_quotes = lambda trade_date: list(FREE_QUOTES)
TushareClient._realtime_reference_cache.clear()
dashboard = self.client._realtime_dashboard("20260720", "20260720", "20260717")
self.assertEqual(dashboard["meta"]["quote_source"], "eastmoney_clist")
self.assertEqual(dashboard["meta"]["quote_source"], "datahub")
self.assertEqual(str(dashboard["meta"]["trade_date"]).replace("-", ""), "20260720")
def test_rt_k_and_free_source_failure_keeps_today_error(self):
def test_hub_failure_keeps_today_error(self):
original_query = self.client.query
def query(api_name, params=None, fields=""):
if api_name == "rt_k":
raise TushareError("没有接口访问权限")
raise TushareError("数据中枢行情暂不可用")
return original_query(api_name, params, fields)
self.client.query = query
self.client.realtime_aggregator = FakeFreeAggregator(fail=True)
TushareClient._realtime_reference_cache.clear()
with self.assertRaises(TushareError) as ctx:
self.client._realtime_dashboard("20260720", "20260720", "20260717")
self.assertIn("当天盘中实时行情不可用", str(ctx.exception))
self.assertIn("没有接口访问权限", str(ctx.exception))
def test_rt_k_and_eastmoney_failure_falls_back_to_tencent(self):
def test_hub_failover_is_invisible_to_website(self):
original_query = self.client.query
def query(api_name, params=None, fields=""):
@@ -318,23 +315,13 @@ class RealtimeDashboardTests(unittest.TestCase):
raise TushareError("没有接口访问权限")
return original_query(api_name, params, fields)
class TencentOnlyAggregator(FakeFreeAggregator):
def eastmoney_market_quotes(self, expected_date=""):
raise RealtimeAggregateError("eastmoney blocked")
def tencent_market_quotes(self, codes, expected_date=""):
return list(FREE_QUOTES)
def tencent_stock_quotes(self, codes, expected_date="", minimum=None):
return list(FREE_QUOTES)
self.client.query = query
self.client.realtime_aggregator = TencentOnlyAggregator()
self.client.try_market_quotes = lambda trade_date: list(FREE_QUOTES)
TushareClient._realtime_reference_cache.clear()
dashboard = self.client._realtime_dashboard("20260720", "20260720", "20260717")
self.assertEqual(dashboard["meta"]["quote_source"], "tencent_qt")
self.assertEqual(dashboard["meta"]["quote_source"], "datahub")
self.assertEqual(str(dashboard["meta"]["trade_date"]).replace("-", ""), "20260720")
self.assertIn("腾讯免费实时", dashboard["meta"]["notice"])
self.assertIn("数据中枢", dashboard["meta"]["notice"])
self.assertEqual(dashboard["overview"]["amount_billion"], 6.0)
def test_normalize_eastmoney_quote_maps_units_and_exchange(self):
@@ -433,6 +420,13 @@ class RealtimeDashboardTests(unittest.TestCase):
def __init__(self):
self.calls = []
def query_api(self, api_name, params=None, fields=""):
rows = FakeRealtimeClient("tok").query(api_name, params or {}, fields)
return DatahubResponse(
data=rows,
meta={"source": "datahub", "stale": False, "staleness_seconds": 0, "row_shape": "tushare"},
)
def quotes_latest(self, **params):
return self.get("/v1/quotes/latest", params)