fix(HEL-494): 修复个股缺失指标、问天遮罩、四爻外显并回补250日K
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
c8a9376adb
commit
3e828b346c
@@ -311,37 +311,37 @@ class ShenwanIndustryMixin:
|
||||
finalized: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
sector_code = str(industry.get("l2_code") or "")
|
||||
sw_rows = self.query(
|
||||
"rt_sw_k",
|
||||
{"ts_code": sector_code},
|
||||
"ts_code,name,trade_time,close,pre_close,high,open,low,vol,amount,pct_change",
|
||||
sw_row, outer_source, outer_error = self._sw_outer_realtime(
|
||||
sector_code,
|
||||
str(industry.get("l2_name") or ""),
|
||||
trade_date,
|
||||
finalized=finalized,
|
||||
)
|
||||
sw_row = sw_rows[0] if sw_rows else {}
|
||||
trade_time = str(sw_row.get("trade_time") or "")
|
||||
quote_date = trade_time[:10].replace("-", "")
|
||||
quote_clock = trade_time[11:19] if len(trade_time) >= 19 else ""
|
||||
trade_time = str(sw_row.get("trade_time") or sw_row.get("quote_time") or "")
|
||||
quote_date = str(sw_row.get("quote_date") or trade_time[:10].replace("-", ""))
|
||||
quote_clock = trade_time[11:19] if len(trade_time) >= 19 else str(sw_row.get("quote_clock") or "")
|
||||
outer_precise = bool(sw_row and quote_date == trade_date)
|
||||
if finalized and (not quote_clock or quote_clock < "15:00:00"):
|
||||
if finalized and quote_clock and quote_clock < "15:00:00":
|
||||
outer_precise = False
|
||||
official_change = _number(sw_row.get("pct_change"))
|
||||
official_change = _number(sw_row.get("pct_change") if sw_row.get("pct_change") not in (None, "") else sw_row.get("change"))
|
||||
if not official_change:
|
||||
close = _number(sw_row.get("close"))
|
||||
pre_close = _number(sw_row.get("pre_close"))
|
||||
close = _number(sw_row.get("close") if sw_row.get("close") not in (None, "") else sw_row.get("price"))
|
||||
pre_close = _number(sw_row.get("pre_close") if sw_row.get("pre_close") not in (None, "") else sw_row.get("previous_close"))
|
||||
official_change = (close / pre_close - 1) * 100 if close and pre_close else 0
|
||||
if not outer_precise:
|
||||
official_change = None
|
||||
outer_error = ""
|
||||
if not sw_row:
|
||||
outer_error = f"No Shenwan realtime index returned for {sector_code}"
|
||||
elif quote_date != trade_date:
|
||||
outer_error = f"Shenwan realtime index date is {quote_date or 'unknown'}, expected {trade_date}"
|
||||
elif finalized and (not quote_clock or quote_clock < "15:00:00"):
|
||||
outer_error = f"Shenwan realtime index is not a close snapshot ({trade_time})"
|
||||
if not sw_row and not outer_error:
|
||||
outer_error = f"申万行业 {sector_code} 当日外显待盘后正式数据或免费实时源"
|
||||
elif quote_date and quote_date != trade_date:
|
||||
outer_error = f"申万实时行业日期是 {quote_date},期望 {trade_date}"
|
||||
elif finalized and quote_clock and quote_clock < "15:00:00":
|
||||
outer_error = f"申万行业尚未形成收盘快照({trade_time})"
|
||||
|
||||
valid: list[dict[str, Any]] = []
|
||||
codes: list[str] = []
|
||||
reference: dict[str, Any] = {}
|
||||
inner_error = ""
|
||||
inner_source = "unavailable"
|
||||
try:
|
||||
reference = self._load_realtime_reference(trade_date, previous_trade_date)
|
||||
active_codes = {
|
||||
@@ -354,18 +354,21 @@ class ShenwanIndustryMixin:
|
||||
for row in members
|
||||
if str(row.get("ts_code") or "") in active_codes
|
||||
]
|
||||
if codes:
|
||||
quotes = self.query("rt_k", {"ts_code": ",".join(codes)}, "")
|
||||
for row in quotes:
|
||||
close = _number(row.get("close"))
|
||||
previous_close = _number(row.get("pre_close"))
|
||||
if close <= 0 or previous_close <= 0:
|
||||
continue
|
||||
valid.append({**row, "change": (close / previous_close - 1) * 100})
|
||||
else:
|
||||
quotes, inner_source = self._load_member_realtime_quotes(codes, trade_date)
|
||||
for row in quotes:
|
||||
close = _number(row.get("close"))
|
||||
previous_close = _number(row.get("pre_close"))
|
||||
if close <= 0 or previous_close <= 0:
|
||||
continue
|
||||
valid.append({**row, "change": (close / previous_close - 1) * 100})
|
||||
if not codes:
|
||||
inner_error = f"No active Shenwan members returned for {sector_code}"
|
||||
elif not quotes:
|
||||
inner_error = f"申万成分实时行情暂不可用:{sector_code}"
|
||||
except TushareError as exc:
|
||||
inner_error = str(exc)
|
||||
if "rt_k" in inner_error or "权限" in inner_error:
|
||||
inner_error = "申万成分实时行情暂不可用,已避开无权限接口"
|
||||
|
||||
coverage = len(valid) / max(len(codes), 1) * 100
|
||||
valid_codes = {str(item.get("ts_code") or "") for item in valid}
|
||||
@@ -390,16 +393,16 @@ class ShenwanIndustryMixin:
|
||||
}
|
||||
equal_change = sum(item["change"] for item in valid) / len(valid) if valid else 0
|
||||
amount_billion = sum(_number(item.get("amount")) for item in valid) / 100000000
|
||||
market_rows: list[dict[str, Any]] = []
|
||||
try:
|
||||
self._ensure_realtime_market_cache(trade_date)
|
||||
with self._realtime_reference_lock:
|
||||
market_rows = list(
|
||||
(self._latest_realtime_market.get(trade_date) or {}).get("rows") or []
|
||||
)
|
||||
market_rows = self._ensure_realtime_market_cache(trade_date)
|
||||
except TushareError as exc:
|
||||
market_rows = []
|
||||
inner_precise = False
|
||||
inner_error = inner_error or str(exc)
|
||||
message = str(exc)
|
||||
if "rt_k" in message or "权限" in message:
|
||||
inner_error = inner_error or "全市场实时行情暂不可用,已避开无权限接口"
|
||||
else:
|
||||
inner_error = inner_error or message
|
||||
capital_map = {
|
||||
str(item.get("ts_code") or ""): item
|
||||
for item in reference.get("capital_rows") or []
|
||||
@@ -408,14 +411,17 @@ class ShenwanIndustryMixin:
|
||||
for item in valid:
|
||||
capital = capital_map.get(str(item.get("ts_code") or ""), {})
|
||||
float_share = _number(capital.get("float_share"))
|
||||
if float_share:
|
||||
sector_turnovers.append(_number(item.get("vol")) / float_share / 100)
|
||||
volume = _number(item.get("vol"))
|
||||
if float_share and volume:
|
||||
# 免费源成交量为股;daily_basic.float_share 为万股。
|
||||
sector_turnovers.append(volume / float_share / 100)
|
||||
market_turnovers = []
|
||||
for item in market_rows:
|
||||
capital = capital_map.get(str(item.get("ts_code") or ""), {})
|
||||
float_share = _number(capital.get("float_share"))
|
||||
if float_share:
|
||||
market_turnovers.append(_number(item.get("vol")) / float_share / 100)
|
||||
volume = _number(item.get("vol"))
|
||||
if float_share and volume:
|
||||
market_turnovers.append(volume / float_share / 100)
|
||||
average_turnover = sum(sector_turnovers) / len(sector_turnovers) if sector_turnovers else 0
|
||||
market_turnover = sum(market_turnovers) / len(market_turnovers) if market_turnovers else 0
|
||||
relative_turnover = average_turnover / market_turnover if market_turnover else 0
|
||||
@@ -447,9 +453,9 @@ class ShenwanIndustryMixin:
|
||||
"amount_billion": round(amount_billion, 2),
|
||||
"count": sum(item["change"] >= 9.5 for item in valid),
|
||||
"max_streak": 0,
|
||||
"source": "tushare_rt_sw_k+sw_members_rt_k",
|
||||
"inner_source": "tushare_sw_members+rt_k",
|
||||
"outer_source": "tushare_rt_sw_k",
|
||||
"source": f"{outer_source or 'unavailable'}+{inner_source}",
|
||||
"inner_source": inner_source,
|
||||
"outer_source": outer_source or "unavailable",
|
||||
"taxonomy": "sw_l2",
|
||||
"industry": industry,
|
||||
"trade_date": trade_date,
|
||||
@@ -464,9 +470,68 @@ class ShenwanIndustryMixin:
|
||||
"inner_error": inner_error,
|
||||
"outer_error": outer_error,
|
||||
"schema_version": 6,
|
||||
"methodology": "外显使用申万官方 rt_sw_k;内核独立使用申万成分 rt_k 宽度与相对换手聚合",
|
||||
"methodology": "外显使用已发布 sw_daily 或免费申万实时;内核使用数据中枢/免费实时成分,不调用 rt_sw_k",
|
||||
}
|
||||
|
||||
def _sw_outer_realtime(
|
||||
self,
|
||||
sector_code: str,
|
||||
sector_name: str,
|
||||
trade_date: str,
|
||||
finalized: bool = False,
|
||||
) -> tuple[dict[str, Any], str, str]:
|
||||
aggregator = getattr(self, "realtime_aggregator", None)
|
||||
loader = getattr(aggregator, "eastmoney_shenwan_quote", None) if aggregator else None
|
||||
if callable(loader):
|
||||
try:
|
||||
row = loader(sector_code, expected_date="" if finalized else trade_date)
|
||||
except Exception as exc:
|
||||
message = str(exc)
|
||||
if finalized:
|
||||
return {}, "", f"申万行业 {sector_code} 盘后正式数据待入库"
|
||||
return {}, "", f"免费申万实时暂不可用:{message[:180]}"
|
||||
if row:
|
||||
return dict(row), str(row.get("source") or "eastmoney_sw"), ""
|
||||
if finalized:
|
||||
return {}, "", f"申万行业 {sector_code} 当日盘后正式数据尚未入库"
|
||||
if aggregator and sector_name:
|
||||
try:
|
||||
row = aggregator.eastmoney_sector(sector_name)
|
||||
except Exception as exc:
|
||||
return {}, "", f"免费行业实时暂不可用:{str(exc)[:180]}"
|
||||
if row:
|
||||
return dict(row), str(row.get("source") or "eastmoney_sector"), ""
|
||||
return {}, "", f"申万行业 {sector_code} 当日外显待补充"
|
||||
|
||||
def _load_member_realtime_quotes(
|
||||
self,
|
||||
codes: list[str],
|
||||
trade_date: str,
|
||||
) -> tuple[list[dict[str, Any]], str]:
|
||||
if not codes:
|
||||
return [], "unavailable"
|
||||
hub = getattr(self, "try_quotes", None)
|
||||
if callable(hub):
|
||||
rows = hub(codes) or []
|
||||
if rows:
|
||||
return list(rows), "datahub"
|
||||
try:
|
||||
quotes, source = self._load_realtime_quotes(",".join(codes), trade_date)
|
||||
return quotes, source
|
||||
except TushareError as exc:
|
||||
message = str(exc)
|
||||
if "rt_k" in message or "权限" in message:
|
||||
aggregator = getattr(self, "realtime_aggregator", None)
|
||||
loader = getattr(aggregator, "eastmoney_stock_quotes", None) if aggregator else None
|
||||
if callable(loader):
|
||||
try:
|
||||
rows = loader(codes, expected_date=trade_date)
|
||||
if rows:
|
||||
return list(rows), "eastmoney_ulist"
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
|
||||
|
||||
def _filter_members_by_listing(
|
||||
members: list[dict[str, Any]],
|
||||
|
||||
Reference in New Issue
Block a user