fix(HEL-482): 开盘前分时回退最近交易日,并接通中枢失败回旧通道
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
605f97e5df
commit
3d2c1252f1
@@ -2,7 +2,8 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from backend.features.market.charts import ChartDataError, EastmoneyChartClient
|
||||
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
|
||||
|
||||
|
||||
@@ -72,6 +73,135 @@ class ChartDataProviderTests(unittest.TestCase):
|
||||
self.client.stock_intraday("abc")
|
||||
|
||||
|
||||
class LookbackChartClient(EastmoneyChartClient):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(cache_ttl_seconds=20)
|
||||
self.requests: list[tuple[str, dict[str, str]]] = []
|
||||
|
||||
def _request_json(self, url, params, referer):
|
||||
self.requests.append((url, params))
|
||||
if url == TRENDS_URL and params.get("ndays") == "1":
|
||||
return {"data": {"code": "601318", "name": "中国平安", "preClose": 56.0, "trends": []}}
|
||||
if url == TRENDS_URL and params.get("ndays") == "5":
|
||||
return {"data": {"code": "601318", "name": "中国平安", "preClose": 56.0, "trends": []}}
|
||||
if url == HIS_TRENDS_URL:
|
||||
return {
|
||||
"data": {
|
||||
"code": "601318",
|
||||
"name": "中国平安",
|
||||
"preClose": 55.8,
|
||||
"trends": [
|
||||
"2026-09-07 09:30,55.80,55.90,56.00,55.70,100,5580.00,55.900",
|
||||
"2026-09-07 15:00,56.10,56.20,56.30,56.00,200,11240.00,56.150",
|
||||
"2026-09-08 09:30,0,0,0,0,0,0.00,0",
|
||||
],
|
||||
}
|
||||
}
|
||||
raise ChartDataError("unexpected url")
|
||||
|
||||
|
||||
class ChartLookbackTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
EastmoneyChartClient._cache.clear()
|
||||
self.client = LookbackChartClient()
|
||||
|
||||
def test_empty_today_falls_back_to_latest_available_session(self):
|
||||
payload = self.client.stock_intraday("601318")
|
||||
urls = [url for url, _ in self.client.requests]
|
||||
self.assertEqual(urls[0], TRENDS_URL)
|
||||
self.assertEqual(self.client.requests[0][1]["ndays"], "1")
|
||||
self.assertEqual(urls[1], TRENDS_URL)
|
||||
self.assertEqual(self.client.requests[1][1]["ndays"], "5")
|
||||
self.assertEqual(urls[2], HIS_TRENDS_URL)
|
||||
self.assertEqual(payload["trade_date"], "2026-09-07")
|
||||
self.assertEqual([point["time"] for point in payload["points"]], ["09:30", "15:00"])
|
||||
self.assertEqual(payload["points"][0]["close"], 55.9)
|
||||
|
||||
def test_delay_multiday_can_recover_without_his(self):
|
||||
class DelayFive(EastmoneyChartClient):
|
||||
def __init__(self):
|
||||
super().__init__(cache_ttl_seconds=20)
|
||||
self.requests = []
|
||||
|
||||
def _request_json(self, url, params, referer):
|
||||
self.requests.append((url, params))
|
||||
if params.get("ndays") == "1":
|
||||
return {"data": {"code": "000001", "name": "平安银行", "preClose": 11.7, "trends": []}}
|
||||
return {
|
||||
"data": {
|
||||
"code": "000001",
|
||||
"name": "平安银行",
|
||||
"preClose": 11.5,
|
||||
"trends": [
|
||||
"2026-09-07 09:30,11.50,11.60,11.70,11.40,100,1160.00,11.600",
|
||||
"2026-09-07 15:00,11.70,11.80,11.90,11.60,200,2360.00,11.750",
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
EastmoneyChartClient._cache.clear()
|
||||
client = DelayFive()
|
||||
payload = client.stock_intraday("000001")
|
||||
self.assertEqual(payload["trade_date"], "2026-09-07")
|
||||
self.assertEqual(len(payload["points"]), 2)
|
||||
self.assertEqual([url for url, _ in client.requests], [TRENDS_URL, TRENDS_URL])
|
||||
|
||||
def test_sh_sz_cyb_codes_use_correct_secid(self):
|
||||
for code, secid in (("601318", "1.601318"), ("000001", "0.000001"), ("300750", "0.300750")):
|
||||
EastmoneyChartClient._cache.clear()
|
||||
client = LookbackChartClient()
|
||||
client.stock_intraday(code)
|
||||
self.assertEqual(client.requests[0][1]["secid"], secid)
|
||||
|
||||
|
||||
class FakeHub:
|
||||
def __init__(self, chart=None, error=None):
|
||||
self.chart = chart
|
||||
self.error = error
|
||||
self.calls: list[str] = []
|
||||
|
||||
def try_intraday(self, code):
|
||||
self.calls.append(code)
|
||||
if self.error:
|
||||
raise self.error
|
||||
return self.chart
|
||||
|
||||
|
||||
class DatahubChartFallbackTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
EastmoneyChartClient._cache.clear()
|
||||
|
||||
def test_datahub_success_skips_old_channel(self):
|
||||
hub = FakeHub(
|
||||
{
|
||||
"entity_type": "stock",
|
||||
"identifier": "601318",
|
||||
"name": "中国平安",
|
||||
"code": "601318",
|
||||
"trade_date": "2026-09-08",
|
||||
"previous_close": 56.36,
|
||||
"points": [{"date": "2026-09-08", "time": "09:30", "close": 56.5, "average": 56.4}],
|
||||
"source": "datahub",
|
||||
}
|
||||
)
|
||||
fallback = LookbackChartClient()
|
||||
client = MarketChartClient(IfindHttpClient(), fallback, hub)
|
||||
payload = client.stock_intraday("601318")
|
||||
self.assertEqual(payload["source"], "datahub")
|
||||
self.assertEqual(hub.calls, ["601318"])
|
||||
self.assertEqual(fallback.requests, [])
|
||||
|
||||
def test_datahub_timeout_or_empty_falls_back_to_eastmoney(self):
|
||||
fallback = LookbackChartClient()
|
||||
for hub in (FakeHub(chart=None), FakeHub(error=RuntimeError("timeout"))):
|
||||
EastmoneyChartClient._cache.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)
|
||||
|
||||
|
||||
class ChartServiceStub:
|
||||
@staticmethod
|
||||
def _payload(code: str, name: str):
|
||||
|
||||
@@ -349,6 +349,55 @@ class DatahubBridgeTests(unittest.TestCase):
|
||||
self.assertEqual(rows[0]["amount"], 2000.0)
|
||||
self.assertEqual(len(legacy.calls), 1)
|
||||
|
||||
def test_try_intraday_respects_switch_and_falls_back_on_bad_payload(self) -> None:
|
||||
closed = DatahubBridge(flags(), FakeClient(error=DatahubError("INTERNAL", "should not run")))
|
||||
self.assertIsNone(closed.try_intraday("601318"))
|
||||
|
||||
empty = DatahubBridge(
|
||||
flags(intraday=(True, False)),
|
||||
FakeClient(response=DatahubResponse(data={"points": []}, meta={"stale": False})),
|
||||
)
|
||||
self.assertIsNone(empty.try_intraday("601318"))
|
||||
|
||||
stale = DatahubBridge(
|
||||
flags(intraday=(True, False)),
|
||||
FakeClient(response=DatahubResponse(
|
||||
data={
|
||||
"entity_type": "stock",
|
||||
"code": "601318",
|
||||
"trade_date": "2026-09-07",
|
||||
"previous_close": 55.8,
|
||||
"points": [{"date": "2026-09-07", "time": "09:30", "close": 55.9, "avg_price": 55.85}],
|
||||
},
|
||||
meta={"stale": True},
|
||||
)),
|
||||
)
|
||||
self.assertIsNone(stale.try_intraday("601318"))
|
||||
|
||||
ok = DatahubBridge(
|
||||
flags(intraday=(True, False)),
|
||||
FakeClient(response=DatahubResponse(
|
||||
data={
|
||||
"entity_type": "stock",
|
||||
"identifier": "601318",
|
||||
"name": "中国平安",
|
||||
"code": "601318",
|
||||
"trade_date": "2026-09-08",
|
||||
"previous_close": 56.36,
|
||||
"points": [
|
||||
{"date": "2026-09-08", "time": "09:30", "close": 0},
|
||||
{"date": "2026-09-08", "time": "09:31", "close": 56.5, "avg_price": 56.4},
|
||||
],
|
||||
},
|
||||
meta={"stale": False},
|
||||
)),
|
||||
)
|
||||
chart = ok.try_intraday("601318")
|
||||
self.assertEqual(chart["source"], "datahub")
|
||||
self.assertEqual(len(chart["points"]), 1)
|
||||
self.assertEqual(chart["points"][0]["average"], 56.4)
|
||||
self.assertEqual(ok.client.paths, ["/v1/intraday/points"])
|
||||
|
||||
def test_features_do_not_import_datahub_client(self) -> None:
|
||||
violations = []
|
||||
for path in (ROOT / "backend" / "features").rglob("*.py"):
|
||||
|
||||
Reference in New Issue
Block a user