fix(HEL-356): support Beijing exchange realtime quotes
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -128,13 +128,19 @@ class EastmoneyAdapter(MarketAdapter):
|
|||||||
def fetch_quotes(self, codes: list[str]) -> list[dict[str, Any]]:
|
def fetch_quotes(self, codes: list[str]) -> list[dict[str, Any]]:
|
||||||
# Eastmoney ulist.np accepts ~60 secids per request; page remaining codes.
|
# Eastmoney ulist.np accepts ~60 secids per request; page remaining codes.
|
||||||
secids = []
|
secids = []
|
||||||
|
suffixes: dict[str, str] = {}
|
||||||
for code in codes:
|
for code in codes:
|
||||||
ts = str(code or "").upper()
|
ts = str(code or "").upper()
|
||||||
symbol = ts.split(".")[0]
|
symbol = ts.split(".")[0]
|
||||||
if ts.endswith(".SH") or symbol.startswith(("5", "6", "9")):
|
if ts.endswith(".BJ") or symbol.startswith(("4", "8", "92")):
|
||||||
|
secids.append(f"0.{symbol}")
|
||||||
|
suffixes[symbol] = "BJ"
|
||||||
|
elif ts.endswith(".SH") or symbol.startswith(("5", "6", "9")):
|
||||||
secids.append(f"1.{symbol}")
|
secids.append(f"1.{symbol}")
|
||||||
|
suffixes[symbol] = "SH"
|
||||||
else:
|
else:
|
||||||
secids.append(f"0.{symbol}")
|
secids.append(f"0.{symbol}")
|
||||||
|
suffixes[symbol] = "SZ"
|
||||||
if not secids:
|
if not secids:
|
||||||
return []
|
return []
|
||||||
result: list[dict[str, Any]] = []
|
result: list[dict[str, Any]] = []
|
||||||
@@ -154,7 +160,7 @@ class EastmoneyAdapter(MarketAdapter):
|
|||||||
symbol = str(row.get("f12") or "")
|
symbol = str(row.get("f12") or "")
|
||||||
if not symbol:
|
if not symbol:
|
||||||
continue
|
continue
|
||||||
ts_code = f"{symbol}.SH" if symbol.startswith(("5", "6", "9")) else f"{symbol}.SZ"
|
ts_code = f"{symbol}.{suffixes.get(symbol, 'SZ')}"
|
||||||
epoch = int(finite_number(row.get("f124")) or 0)
|
epoch = int(finite_number(row.get("f124")) or 0)
|
||||||
close = round4(finite_number(row.get("f2")))
|
close = round4(finite_number(row.get("f2")))
|
||||||
previous = round4(finite_number(row.get("f18")))
|
previous = round4(finite_number(row.get("f18")))
|
||||||
@@ -462,10 +468,10 @@ def _normalize_market_quote(row: dict[str, Any]) -> dict[str, Any] | None:
|
|||||||
if close <= 0 or previous_close <= 0:
|
if close <= 0 or previous_close <= 0:
|
||||||
return None
|
return None
|
||||||
market = int(finite_number(row.get("f13")) or 0)
|
market = int(finite_number(row.get("f13")) or 0)
|
||||||
if market == 1 or symbol.startswith(("5", "6", "9")):
|
if market == 0 and symbol.startswith(("4", "8", "92")):
|
||||||
ts_code = f"{symbol}.SH"
|
|
||||||
elif symbol.startswith(("4", "8")):
|
|
||||||
ts_code = f"{symbol}.BJ"
|
ts_code = f"{symbol}.BJ"
|
||||||
|
elif market == 1 or symbol.startswith(("5", "6", "9")):
|
||||||
|
ts_code = f"{symbol}.SH"
|
||||||
else:
|
else:
|
||||||
ts_code = f"{symbol}.SZ"
|
ts_code = f"{symbol}.SZ"
|
||||||
epoch = int(finite_number(row.get("f124")) or 0)
|
epoch = int(finite_number(row.get("f124")) or 0)
|
||||||
|
|||||||
@@ -158,10 +158,10 @@ def _tencent_symbol(code: str) -> str:
|
|||||||
symbol = raw.split(".")[0]
|
symbol = raw.split(".")[0]
|
||||||
if not symbol.isdigit() or len(symbol) != 6:
|
if not symbol.isdigit() or len(symbol) != 6:
|
||||||
return ""
|
return ""
|
||||||
|
if raw.endswith(".BJ") or symbol.startswith(("4", "8", "92")):
|
||||||
|
return f"bj{symbol}"
|
||||||
if raw.endswith(".SH") or symbol.startswith(("5", "6", "9")):
|
if raw.endswith(".SH") or symbol.startswith(("5", "6", "9")):
|
||||||
return f"sh{symbol}"
|
return f"sh{symbol}"
|
||||||
if raw.endswith(".BJ") or symbol.startswith(("4", "8")):
|
|
||||||
return f"bj{symbol}"
|
|
||||||
return f"sz{symbol}"
|
return f"sz{symbol}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -616,6 +616,8 @@ def _guess_ts_code(code: str) -> str | None:
|
|||||||
if "." in raw:
|
if "." in raw:
|
||||||
return raw
|
return raw
|
||||||
if len(raw) == 6 and raw.isdigit():
|
if len(raw) == 6 and raw.isdigit():
|
||||||
|
if raw.startswith(("4", "8", "92")):
|
||||||
|
return f"{raw}.BJ"
|
||||||
if raw.startswith(("5", "6", "9")):
|
if raw.startswith(("5", "6", "9")):
|
||||||
return f"{raw}.SH"
|
return f"{raw}.SH"
|
||||||
return f"{raw}.SZ"
|
return f"{raw}.SZ"
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ class V1API:
|
|||||||
limit, offset = self._page(q)
|
limit, offset = self._page(q)
|
||||||
today = yyyymmdd(now_shanghai())
|
today = yyyymmdd(now_shanghai())
|
||||||
batch_id, snapshot = self.pipeline.published_stock_snapshot(today)
|
batch_id, snapshot = self.pipeline.published_stock_snapshot(today)
|
||||||
if batch_id:
|
if batch_id and snapshot:
|
||||||
# Formal view: the latest published stock snapshot, with batch
|
# Formal view: the latest published stock snapshot, with batch
|
||||||
# metadata. Filters are applied in-memory on the snapshot.
|
# metadata. Filters are applied in-memory on the snapshot.
|
||||||
pub = self.pipeline.latest_stocks_publication(today) or {}
|
pub = self.pipeline.latest_stocks_publication(today) or {}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ from pathlib import Path
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from datahub.adapters.base import AdapterError
|
from datahub.adapters.base import AdapterError
|
||||||
from datahub.adapters.eastmoney import HIS_TRENDS_URL, TRENDS_URL, EastmoneyAdapter
|
from datahub.adapters.eastmoney import HIS_TRENDS_URL, TRENDS_URL, EastmoneyAdapter, _normalize_market_quote
|
||||||
|
from datahub.adapters.tencent import _tencent_symbol
|
||||||
from datahub.db import HubDB
|
from datahub.db import HubDB
|
||||||
from datahub.realtime_serve import fetch_intraday
|
from datahub.realtime_serve import fetch_intraday
|
||||||
from datahub.serving import ApiError, V1API
|
from datahub.serving import ApiError, V1API
|
||||||
@@ -47,6 +48,15 @@ class EastmoneyIntradayLookbackTests(unittest.TestCase):
|
|||||||
self.assertEqual([point["time"] for point in payload["points"]], ["09:30", "15:00"])
|
self.assertEqual([point["time"] for point in payload["points"]], ["09:30", "15:00"])
|
||||||
self.assertEqual(payload["points"][0]["close"], 55.9)
|
self.assertEqual(payload["points"][0]["close"], 55.9)
|
||||||
|
|
||||||
|
def test_beijing_exchange_920_codes_keep_their_market(self):
|
||||||
|
self.assertEqual(_tencent_symbol("920703.BJ"), "bj920703")
|
||||||
|
self.assertEqual(_tencent_symbol("920703"), "bj920703")
|
||||||
|
quote = _normalize_market_quote({
|
||||||
|
"f12": "920703", "f13": 0, "f2": 18.2, "f18": 18.0,
|
||||||
|
"f3": 1.1, "f5": 0, "f6": 0,
|
||||||
|
})
|
||||||
|
self.assertEqual(quote["ts_code"], "920703.BJ")
|
||||||
|
|
||||||
def test_preferred_date_keeps_that_session(self):
|
def test_preferred_date_keeps_that_session(self):
|
||||||
adapter = FakeEastmoney()
|
adapter = FakeEastmoney()
|
||||||
payload = adapter.fetch_intraday("601318.SH", "20260907")
|
payload = adapter.fetch_intraday("601318.SH", "20260907")
|
||||||
|
|||||||
Reference in New Issue
Block a user