fix(HEL-494): 切断网站生产装配外源直连,iFinD 与实时观察改走中枢

生产 gateway 不再实例化 iFinD、东财图和免费实时聚合器;问财与竞价快照作为中枢内部数据源。全站阻断外源测试覆盖日K、报价、图表、问财和竞价快照。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
总工
2026-09-08 22:41:51 +08:00
co-authored by Cursor multica-agent
parent 0b8419abca
commit 100752f43c
24 changed files with 1275 additions and 110 deletions
+3 -2
View File
@@ -45,8 +45,9 @@ class BootstrapContainerTests(unittest.TestCase):
self.assertIs(container.strategy_tracking.repository.database, database)
self.assertIs(container.alert_service.repository.database, database)
self.assertIs(container.trade_journal.repository.database, database)
self.assertIs(container.chart_data.ifind, container.ifind)
self.assertTrue(container.ifind.configured)
self.assertIs(container.ifind, container.data_gateway.ifind)
self.assertIs(container.chart_data.datahub, container.data_gateway.datahub)
self.assertIsNone(container.chart_data.ifind)
if __name__ == "__main__":
+3 -4
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
import unittest
from backend.data.providers.ifind_client import IfindHttpClient
from backend.features.market.charts import ChartDataError, EastmoneyChartClient, HIS_TRENDS_URL, MarketChartClient, TRENDS_URL
from server import DashboardService
@@ -196,7 +195,7 @@ class DatahubChartFallbackTests(unittest.TestCase):
}
)
fallback = LookbackChartClient()
client = MarketChartClient(IfindHttpClient(), fallback, hub)
client = MarketChartClient(hub)
payload = client.stock_intraday("601318")
self.assertEqual(payload["source"], "datahub")
self.assertEqual(hub.calls, ["601318"])
@@ -212,7 +211,7 @@ class DatahubChartFallbackTests(unittest.TestCase):
):
EastmoneyChartClient._cache.clear()
fallback.requests.clear()
client = MarketChartClient(IfindHttpClient(), fallback, hub)
client = MarketChartClient(hub)
with self.assertRaises(ChartDataError):
client.stock_intraday("000001")
self.assertEqual(fallback.requests, [])
@@ -231,7 +230,7 @@ class DatahubChartFallbackTests(unittest.TestCase):
}
]
)
client = MarketChartClient(IfindHttpClient(), LookbackChartClient(), hub)
client = MarketChartClient(hub)
rows = client.stock_daily("600000", "20260907")
self.assertEqual(rows[-1]["trade_date"], "2026-09-07")
self.assertIn("daily:600000", hub.calls)
+20 -5
View File
@@ -35,7 +35,7 @@ class DataGatewayTests(unittest.TestCase):
with self.assertRaises(DataPolicyError):
policy.assert_allowed("market.level2", "unresolved", "display")
def test_gateway_uses_live_token_supplier_and_shared_ifind(self) -> None:
def test_gateway_uses_live_token_supplier_and_hub_proxies(self) -> None:
token = {"value": "first"}
gateway = build_data_gateway(
{"ifind_refresh_token": "refresh", "ifind_access_token": "access"},
@@ -44,7 +44,14 @@ class DataGatewayTests(unittest.TestCase):
self.assertEqual(gateway.tushare().token, "first")
token["value"] = "second"
self.assertEqual(gateway.tushare().token, "second")
self.assertIs(gateway.chart_data.ifind, gateway.ifind)
self.assertIs(gateway.ifind, gateway.ifind_provider.client)
self.assertIs(gateway.chart_data.datahub, gateway.datahub)
self.assertIsNone(gateway.chart_data.ifind)
from backend.data.datahub.ifind_proxy import HubIfindProxy
from backend.data.datahub.realtime_proxy import HubRealtimeProxy
self.assertIsInstance(gateway.ifind, HubIfindProxy)
self.assertIsInstance(gateway.realtime_observer, HubRealtimeProxy)
def test_server_has_no_direct_runtime_tushare_construction(self) -> None:
source = (
@@ -60,18 +67,23 @@ class DataGatewayTests(unittest.TestCase):
def test_provider_construction_has_unique_declared_owners(self) -> None:
root = Path(__file__).resolve().parents[1]
owners = {
"EastmoneyChartClient": {"backend/data/gateway.py"},
"IfindHttpClient": {"backend/data/gateway.py"},
"IfindProvider": {"backend/data/gateway.py"},
"MarketChartClient": {"backend/data/gateway.py"},
"TushareClient": {"backend/features/market/service.py"},
"TushareProvider": {"backend/data/gateway.py"},
"WebRealtimeAggregator": {"backend/data/gateway.py"},
"DatahubClient": {"backend/data/gateway.py"},
"DatahubAwareTushareClient": {"backend/data/gateway.py"},
"DatahubBridge": {"backend/data/gateway.py"},
"HubIfindProxy": {"backend/data/gateway.py"},
"HubRealtimeProxy": {"backend/data/gateway.py"},
}
found = {name: set() for name in owners}
forbidden = {
"IfindHttpClient": set(),
"EastmoneyChartClient": set(),
"WebRealtimeAggregator": set(),
}
found_forbidden = {name: set() for name in forbidden}
for path in (root / "backend").rglob("*.py"):
relative = path.relative_to(root).as_posix()
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
@@ -81,7 +93,10 @@ class DataGatewayTests(unittest.TestCase):
name = getattr(node.func, "id", None) or getattr(node.func, "attr", None)
if name in found:
found[name].add(relative)
if name in found_forbidden:
found_forbidden[name].add(relative)
self.assertEqual(found, owners)
self.assertEqual(found_forbidden, forbidden)
provider_source = (root / "backend/data/providers/tushare.py").read_text(
encoding="utf-8"
)
+235 -30
View File
@@ -1,14 +1,19 @@
from __future__ import annotations
import ast
import json
import unittest
from pathlib import Path
from unittest.mock import patch
from backend.data import build_data_gateway
from backend.data.datahub.bridge import DatahubAwareTushareClient, DatahubBridge
from backend.data.datahub.client import DatahubClient
from backend.data.datahub.ifind_proxy import HubIfindProxy
from backend.data.datahub.realtime_proxy import HubRealtimeProxy
from backend.data.datahub.settings import DATASETS, DatahubSettings, DatasetFlags
from backend.data.providers.tushare_transport import TushareError
from backend.features.market.charts import MarketChartClient
from tests.test_datahub_bridge import FakeClient, FakeLegacy, flags
@@ -23,7 +28,174 @@ BLOCKED_HOSTS = (
"hq.sinajs.cn",
"10jqka.com.cn",
"xuangubao.cn",
"quantapi.51ifind.com",
"51ifind.com",
)
LEFTOVER_WEBSITE_FILES = {
"backend/data/providers/ifind_client.py",
"backend/data/realtime.py",
"backend/features/market/charts.py",
"backend/data/providers/tushare_transport.py",
}
HUB_BASE = "http://127.0.0.1:8766"
def _enabled_settings() -> DatahubSettings:
return DatahubSettings(
base_url=HUB_BASE,
token="hub-token",
datasets={name: DatasetFlags(name, read=True) for name in DATASETS},
)
class _Resp:
def __init__(self, payload: dict) -> None:
self.status = 200
self._raw = json.dumps(payload).encode("utf-8")
def read(self):
return self._raw
def __enter__(self):
return self
def __exit__(self, *args):
return False
def hub_payload(request) -> dict:
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 HUB_BASE not in url:
raise AssertionError(f"unexpected url: {url}")
path = url.split(HUB_BASE, 1)[1].split("?", 1)[0]
if path == "/v1/bars/daily":
return {
"schema_version": 1,
"data": [
{
"ts_code": "600000.SH",
"trade_date": "20240902",
"open": 10.0,
"high": 10.4,
"low": 9.9,
"close": 10.2,
"volume": 100000,
"amount": 2000000,
}
],
"meta": {"stale": False, "staleness_seconds": 0, "source": "datahub"},
}
if path == "/v1/quotes/latest":
return {
"schema_version": 1,
"data": [
{
"ts_code": "600000.SH",
"code": "600000",
"name": "浦发银行",
"close": 10.2,
"price": 10.2,
"pre_close": 10.0,
"open": 10.1,
"high": 10.3,
"low": 9.9,
"vol": 1000,
"amount": 2000000,
"quote_date": "20240902",
"source": "datahub",
}
],
"meta": {"stale": False, "staleness_seconds": 0, "source": "eastmoney"},
}
if path == "/v1/indexes/quotes":
return {
"schema_version": 1,
"data": [
{
"ts_code": "000001.SH",
"code": "000001",
"name": "上证指数",
"price": 3000,
"pct_chg": 1.2,
"quote_time_epoch": 1725249600,
"source": "datahub",
},
{
"ts_code": "399001.SZ",
"code": "399001",
"name": "深证成指",
"price": 9000,
"pct_chg": 0.8,
"quote_time_epoch": 1725249600,
"source": "datahub",
},
{
"ts_code": "399006.SZ",
"code": "399006",
"name": "创业板指",
"price": 1800,
"pct_chg": 0.5,
"quote_time_epoch": 1725249600,
"source": "datahub",
},
],
"meta": {"stale": False, "staleness_seconds": 0, "source": "tencent"},
}
if path == "/v1/intraday/points":
return {
"schema_version": 1,
"data": {
"entity_type": "stock",
"identifier": "600000",
"code": "600000",
"trade_date": "2024-09-02",
"previous_close": 10.0,
"points": [
{"date": "2024-09-02", "time": "09:30", "close": 10.2, "average": 10.1, "open": 10.1, "high": 10.2, "low": 10.0, "volume": 100, "amount": 1000}
],
},
"meta": {"stale": False, "source": "datahub"},
}
if path == "/v1/query":
body = json.loads(request.data.decode("utf-8") if request.data else "{}")
api_name = body.get("api_name")
if api_name == "ifind_status":
return {"schema_version": 1, "data": [{"configured": True, "access_ready": True, "access_expires_at": ""}], "meta": {"source": "ifind"}}
if api_name == "ifind_wencai":
return {
"schema_version": 1,
"data": [{"股票代码": "000001.SZ", "涨停原因": "重组", "首次涨停时间": "09:42:00"}],
"meta": {"source": "ifind"},
}
if api_name == "ifind_snapshots":
return {
"schema_version": 1,
"data": [
{
"time": "2024-09-02 09:21:00",
"thscode": "000001.SZ",
"latest": 10.5,
"preClose": 10,
"volume": 2000,
"amount": 21000,
}
],
"meta": {"source": "ifind"},
}
if api_name in {"daily", "rt_k"}:
return {
"schema_version": 1,
"data": [{"ts_code": "600000.SH", "trade_date": "20240902", "close": 10.2, "amount": 2000.0}],
"meta": {"source": "datahub", "stale": False, "row_shape": "tushare"},
}
raise AssertionError(f"unexpected query api: {api_name}")
raise AssertionError(f"unexpected path: {path}")
def blocked_urlopen(request, timeout=None):
return _Resp(hub_payload(request))
class HubExclusiveWebsiteTests(unittest.TestCase):
@@ -39,36 +211,7 @@ class HubExclusiveWebsiteTests(unittest.TestCase):
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}")
settings = _enabled_settings()
hub_client = DatahubClient(settings, urlopen=blocked_urlopen)
legacy = FakeLegacy(TushareError("blocked"))
wrapped = DatahubAwareTushareClient(legacy, DatahubBridge(settings, hub_client))
@@ -78,6 +221,56 @@ class HubExclusiveWebsiteTests(unittest.TestCase):
self.assertEqual(rows[0]["amount"], 2000.0)
self.assertEqual(legacy.calls, [])
def test_site_features_only_reach_hub_when_external_hosts_blocked(self) -> None:
settings = _enabled_settings()
hub_client = DatahubClient(settings, urlopen=blocked_urlopen)
bridge = DatahubBridge(settings, hub_client)
with patch("urllib.request.urlopen", blocked_urlopen):
quotes = bridge.try_quotes(["600000.SH"])
indices = bridge.try_index_quotes()
chart = MarketChartClient(bridge).stock_daily("600000", "20240902")
intraday = MarketChartClient(bridge).stock_intraday("600000")
ifind = HubIfindProxy(bridge)
wencai = ifind.wencai("2024年9月2日涨停股票")
snapshots = ifind.snapshots(["000001.SZ"], ["latest"], "2024-09-02 09:15:00", "2024-09-02 09:22:00")
realtime = HubRealtimeProxy(bridge)
index_rows = realtime.tencent_indices()
stock = realtime.tencent_stock_quote("600000.SH", "20240902")
health = realtime.health_snapshot()
self.assertEqual(quotes[0]["close"], 10.2)
self.assertEqual(len(indices), 3)
self.assertEqual(chart[-1]["close"], 10.2)
self.assertEqual(intraday["source"], "datahub")
self.assertEqual(wencai[0]["股票代码"], "000001.SZ")
self.assertEqual(snapshots[0]["latest"], 10.5)
self.assertEqual([row["code"] for row in index_rows], ["000001", "399001", "399006"])
self.assertEqual(stock["close"], 10.2)
self.assertTrue(health["ready"])
self.assertTrue(ifind.configured)
def test_production_gateway_does_not_construct_external_clients(self) -> None:
source = (ROOT / "backend" / "data" / "gateway.py").read_text(encoding="utf-8")
self.assertNotIn("IfindHttpClient", source)
self.assertNotIn("EastmoneyChartClient", source)
self.assertNotIn("WebRealtimeAggregator", source)
self.assertIn("HubIfindProxy", source)
self.assertIn("HubRealtimeProxy", source)
self.assertIn("legacy.realtime_aggregator = None", source)
self.assertIn("DatahubAwareTushareClient", source)
def test_production_python_does_not_embed_blocked_hosts(self) -> None:
violations = []
for path in (ROOT / "backend").rglob("*.py"):
relative = path.relative_to(ROOT).as_posix()
if relative in LEFTOVER_WEBSITE_FILES:
continue
text = path.read_text(encoding="utf-8")
for host in BLOCKED_HOSTS:
if host in text:
violations.append(f"{relative} -> {host}")
self.assertEqual(violations, [])
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)
@@ -101,6 +294,18 @@ class HubExclusiveWebsiteTests(unittest.TestCase):
self.assertTrue(any("query_api" in text for text in called))
self.assertFalse(any("legacy_query" in text for text in called))
def test_build_gateway_uses_hub_proxies_without_opening_external_hosts(self) -> None:
settings = _enabled_settings()
with patch("urllib.request.urlopen", blocked_urlopen):
gateway = build_data_gateway({"tushare_token": "tok"}, datahub_settings=settings)
gateway.datahub.client = DatahubClient(settings, urlopen=blocked_urlopen)
rows = gateway.ifind.wencai("涨停")
quotes = gateway.realtime_observer.tencent_indices()
chart = gateway.chart_data.stock_daily("600000", "20240902")
self.assertEqual(rows[0]["涨停原因"], "重组")
self.assertEqual(len(quotes), 3)
self.assertEqual(chart[-1]["close"], 10.2)
if __name__ == "__main__":
unittest.main()
+3 -3
View File
@@ -6,7 +6,7 @@ from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch
from backend.features.market.charts import EastmoneyChartClient, MarketChartClient
from backend.features.market.charts import MarketChartClient
from database import ReviewDatabase
from backend.features.market.insights import MarketInsightsService
from server import DashboardService
@@ -166,7 +166,7 @@ class IfindFeatureTests(unittest.TestCase):
},
]
)
client = MarketChartClient(FakeIfind(), EastmoneyChartClient(), hub)
client = MarketChartClient(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)
@@ -185,7 +185,7 @@ class IfindFeatureTests(unittest.TestCase):
}
]
)
client = MarketChartClient(FakeIfindStalePreopen(), EastmoneyChartClient(), hub)
client = MarketChartClient(hub)
with patch("backend.features.market.charts.datetime", FixedPreopenDatetime):
rows = client.stock_daily("000001", "20260729")