fix(HEL-356): complete Shenwan industry signal
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -197,17 +197,22 @@ def fetch_sector_quote(db: HubDB, code: str, expected_date: str = "") -> dict[st
|
||||
if ts_code.isdigit():
|
||||
ts_code = f"{ts_code}.SI"
|
||||
cache_key = f"sector:{ts_code}"
|
||||
canonical_name = _canonical_sw_name(db, ts_code)
|
||||
cached = _read_cache(db, cache_key)
|
||||
if cached is not None:
|
||||
if cached is not None and _sector_payload_matches(cached, canonical_name):
|
||||
return cached
|
||||
errors: list[str] = []
|
||||
try:
|
||||
row = EastmoneyAdapter().fetch_shenwan_quote(ts_code)
|
||||
if not _sector_row_matches(row, canonical_name):
|
||||
raise AdapterError(
|
||||
f"industry name mismatch: expected {canonical_name}, got {row.get('name') or '--'}"
|
||||
)
|
||||
source = str(row.get("source") or "eastmoney_sw")
|
||||
except Exception as exc:
|
||||
errors.append(f"eastmoney:{exc}")
|
||||
recovered = _load_quotes_lkg(db, cache_key, expected_date)
|
||||
if recovered is not None:
|
||||
if recovered is not None and _sector_payload_matches(recovered, canonical_name):
|
||||
return recovered
|
||||
raise RealtimeApiError(
|
||||
"SOURCE_UNAVAILABLE",
|
||||
@@ -217,7 +222,7 @@ def fetch_sector_quote(db: HubDB, code: str, expected_date: str = "") -> dict[st
|
||||
quote_date = str(row.get("quote_date") or "")
|
||||
if want and quote_date and quote_date != want:
|
||||
recovered = _load_quotes_lkg(db, cache_key, want)
|
||||
if recovered is not None:
|
||||
if recovered is not None and _sector_payload_matches(recovered, canonical_name):
|
||||
return recovered
|
||||
raise RealtimeApiError("SOURCE_UNAVAILABLE", f"sector quote date {quote_date} != {want}")
|
||||
payload = _envelope(
|
||||
@@ -235,6 +240,29 @@ def fetch_sector_quote(db: HubDB, code: str, expected_date: str = "") -> dict[st
|
||||
return payload
|
||||
|
||||
|
||||
def _canonical_sw_name(db: HubDB, ts_code: str) -> str:
|
||||
row = db.fetchone(
|
||||
"SELECT name FROM sector_master WHERE ts_code = ? AND family = 'sw'",
|
||||
(ts_code,),
|
||||
)
|
||||
if not row:
|
||||
row = db.fetchone(
|
||||
"SELECT name FROM eod_sector_daily WHERE ts_code = ? AND family = 'sw' "
|
||||
"ORDER BY trade_date DESC LIMIT 1",
|
||||
(ts_code,),
|
||||
)
|
||||
return str((row or {}).get("name") or "").strip()
|
||||
|
||||
|
||||
def _sector_row_matches(row: dict[str, Any], canonical_name: str) -> bool:
|
||||
return not canonical_name or str(row.get("name") or "").strip() == canonical_name
|
||||
|
||||
|
||||
def _sector_payload_matches(payload: dict[str, Any], canonical_name: str) -> bool:
|
||||
data = payload.get("data") if isinstance(payload, dict) else None
|
||||
return isinstance(data, dict) and _sector_row_matches(data, canonical_name)
|
||||
|
||||
|
||||
def fetch_limit_pool(db: HubDB, trade_date: str = "") -> dict[str, Any]:
|
||||
day = yyyymmdd(trade_date or now_shanghai())
|
||||
cache_key = f"limit-pool:{day}"
|
||||
@@ -489,18 +517,29 @@ def warm_realtime(db: HubDB) -> dict[str, Any]:
|
||||
try:
|
||||
masters = db.fetchall(
|
||||
"""
|
||||
SELECT ts_code FROM sector_master WHERE family = 'sw'
|
||||
UNION
|
||||
SELECT ts_code FROM eod_sector_daily WHERE family = 'sw'
|
||||
ORDER BY ts_code
|
||||
SELECT ts_code, name, 0 AS priority FROM sector_master WHERE family = 'sw'
|
||||
UNION ALL
|
||||
SELECT ts_code, name, 1 AS priority FROM eod_sector_daily WHERE family = 'sw'
|
||||
ORDER BY priority, ts_code
|
||||
"""
|
||||
)
|
||||
codes = [str(row.get("ts_code") or "") for row in masters if row.get("ts_code")]
|
||||
sector_rows = _eastmoney_sector_quotes(codes)
|
||||
for row in sector_rows:
|
||||
canonical_names: dict[str, str] = {}
|
||||
for master in masters:
|
||||
master_code = str(master.get("ts_code") or "")
|
||||
master_name = str(master.get("name") or "").strip()
|
||||
if master_code and master_name:
|
||||
canonical_names.setdefault(master_code, master_name)
|
||||
codes = list(canonical_names)
|
||||
fetched_sector_rows = _eastmoney_sector_quotes(codes)
|
||||
for row in fetched_sector_rows:
|
||||
if _row_quote_date(row, today) != today:
|
||||
continue
|
||||
code = str(row.get("ts_code") or "").upper()
|
||||
if not _sector_row_matches(row, canonical_names.get(code, "")):
|
||||
result["errors"].append(
|
||||
f"sector:{code}:industry name mismatch"
|
||||
)
|
||||
continue
|
||||
payload = _envelope(
|
||||
row,
|
||||
{
|
||||
@@ -513,6 +552,7 @@ def warm_realtime(db: HubDB) -> dict[str, Any]:
|
||||
},
|
||||
)
|
||||
_write_cache(db, f"sector:{code}", payload, INDEX_TTL, "eastmoney_sw")
|
||||
sector_rows.append(row)
|
||||
result["sectors"] = len(sector_rows)
|
||||
result["rows"] += len(sector_rows)
|
||||
except Exception as exc:
|
||||
|
||||
@@ -9,7 +9,7 @@ from datahub.adapters.base import AdapterError
|
||||
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.realtime_serve import fetch_intraday
|
||||
from datahub.realtime_serve import fetch_intraday, fetch_sector_quote
|
||||
from datahub.serving import ApiError, V1API
|
||||
from datahub.timeutil import now_shanghai, yyyymmdd
|
||||
|
||||
@@ -110,6 +110,33 @@ class IntradayLkgTests(unittest.TestCase):
|
||||
self.assertIn("intraday unavailable", str(ctx.exception))
|
||||
|
||||
|
||||
class SectorQuoteIdentityTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.db = HubDB(Path(self.tmp.name) / "hub.db")
|
||||
self.db.execute(
|
||||
"INSERT INTO sector_master(ts_code,name,family,updated_at) VALUES (?,?,?,?)",
|
||||
("801074.SI", "专用设备", "sw", "2026-09-09T10:00:00+08:00"),
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.tmp.cleanup()
|
||||
|
||||
def test_rejects_same_numeric_code_from_different_taxonomy(self) -> None:
|
||||
wrong = {
|
||||
"ts_code": "801074.SI",
|
||||
"name": "托育服务",
|
||||
"change": -2.19,
|
||||
"quote_date": "20260909",
|
||||
"source": "eastmoney_sw",
|
||||
}
|
||||
with patch("datahub.realtime_serve.EastmoneyAdapter") as mocked:
|
||||
mocked.return_value.fetch_shenwan_quote.return_value = wrong
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
fetch_sector_quote(self.db, "801074.SI", "20260909")
|
||||
self.assertIn("industry name mismatch", str(ctx.exception))
|
||||
|
||||
|
||||
class ServingIntradayDateTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
|
||||
Reference in New Issue
Block a user