fix(HEL-494): 日K默认45根并修复问天行业0/205覆盖
悬浮窗和详情页只画最近45个交易日,中枢仍保留250日历史。盘后缺sw_daily时保留成分日线内核,外显走免费申万;成分行情改为全市场快照+分页,不再截成前60只。 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
3e828b346c
commit
b5d65ecb41
@@ -148,7 +148,7 @@ class DatahubBridge:
|
||||
cleaned = [str(item or "").strip() for item in codes if str(item or "").strip()]
|
||||
if not cleaned:
|
||||
return None
|
||||
return self._try_quote_rows("quotes", {"codes": ",".join(cleaned[:60])}, minimum=1)
|
||||
return self._try_quote_rows("quotes", {"codes": ",".join(cleaned)}, minimum=1)
|
||||
|
||||
def try_index_quotes(self) -> list[dict[str, Any]] | None:
|
||||
flags = self.settings.flags("index_quotes")
|
||||
@@ -208,7 +208,7 @@ class DatahubBridge:
|
||||
if not rows:
|
||||
raise DatahubError("EMPTY", f"{dataset} chart empty")
|
||||
self._record_route(dataset, "datahub", str((response.meta or {}).get("source") or "datahub"))
|
||||
return rows[-max(20, min(180, int(limit))):]
|
||||
return rows[-max(1, int(limit)):]
|
||||
except Exception as exc:
|
||||
self._log_failure(dataset, exc)
|
||||
return None
|
||||
|
||||
@@ -132,21 +132,63 @@ class ShenwanIndustryMixin:
|
||||
actual_trade_date = str(daily.get("trade_date") or "")
|
||||
outer_precise = actual_trade_date == trade_date
|
||||
outer_error = "" if outer_precise else (
|
||||
f"No Shenwan daily returned for {sector_code} on {trade_date}"
|
||||
f"申万行业 {sector_code} 当日盘后正式数据尚未入库"
|
||||
)
|
||||
outer_source = "tushare_sw_daily" if outer_precise else "unavailable"
|
||||
if not outer_precise and allow_realtime_close:
|
||||
try:
|
||||
return self._sw_realtime_sector_snapshot(
|
||||
industry,
|
||||
members,
|
||||
inner_ok = bool(member_rows) and not coverage_issue
|
||||
if inner_ok:
|
||||
sw_row, rt_source, rt_error = self._sw_outer_realtime(
|
||||
sector_code,
|
||||
str(industry.get("l2_name") or ""),
|
||||
trade_date,
|
||||
previous_trade_date,
|
||||
finalized=True,
|
||||
)
|
||||
except TushareError as exc:
|
||||
outer_error = f"{outer_error}; realtime close fallback failed: {exc}"
|
||||
if sw_row:
|
||||
daily = sw_row
|
||||
actual_trade_date = str(
|
||||
sw_row.get("quote_date") or sw_row.get("trade_date") or ""
|
||||
)
|
||||
trade_time = str(sw_row.get("trade_time") or sw_row.get("quote_time") or "")
|
||||
quote_clock = (
|
||||
trade_time[11:19]
|
||||
if len(trade_time) >= 19
|
||||
else str(sw_row.get("quote_clock") or "")
|
||||
)
|
||||
outer_precise = actual_trade_date == trade_date
|
||||
if quote_clock and quote_clock < "15:00:00":
|
||||
outer_precise = False
|
||||
outer_source = rt_source or "eastmoney_sw"
|
||||
outer_error = "" if outer_precise else (
|
||||
rt_error or f"申万行业 {sector_code} 免费实时尚未形成收盘快照"
|
||||
)
|
||||
else:
|
||||
outer_error = rt_error or outer_error
|
||||
else:
|
||||
try:
|
||||
snapshot = self._sw_realtime_sector_snapshot(
|
||||
industry,
|
||||
members,
|
||||
trade_date,
|
||||
previous_trade_date,
|
||||
finalized=True,
|
||||
)
|
||||
snapshot.update({
|
||||
"raw_member_count": raw_member_count,
|
||||
"excluded_member_count": len(excluded_members),
|
||||
"excluded_members": excluded_members,
|
||||
})
|
||||
return snapshot
|
||||
except TushareError:
|
||||
outer_error = f"{outer_error}; 免费实时成分暂不可用"
|
||||
|
||||
official_change = _number(daily.get("pct_change")) if outer_precise else None
|
||||
official_change = None
|
||||
if outer_precise:
|
||||
official_change = _number(
|
||||
daily.get("pct_change")
|
||||
if daily.get("pct_change") not in (None, "")
|
||||
else daily.get("change")
|
||||
)
|
||||
return {
|
||||
"code": sector_code,
|
||||
"name": industry.get("l2_name") or daily.get("name") or sector_code,
|
||||
@@ -173,9 +215,9 @@ class ShenwanIndustryMixin:
|
||||
"amount_billion": round(amount_billion, 2),
|
||||
"count": 0,
|
||||
"max_streak": 0,
|
||||
"source": "tushare_sw_daily+member_daily" if outer_precise else "tushare_member_daily",
|
||||
"source": f"{outer_source}+tushare_member_daily" if outer_precise else "tushare_member_daily",
|
||||
"inner_source": "tushare_member_daily",
|
||||
"outer_source": "tushare_sw_daily" if outer_precise else "unavailable",
|
||||
"outer_source": outer_source,
|
||||
"taxonomy": "sw_l2",
|
||||
"industry": industry,
|
||||
"trade_date": trade_date,
|
||||
@@ -189,7 +231,7 @@ class ShenwanIndustryMixin:
|
||||
"inner_error": inner_error,
|
||||
"outer_error": outer_error,
|
||||
"schema_version": 6,
|
||||
"methodology": "外显使用申万二级行业官方日线;内核独立使用当日成分日线宽度与等权涨跌聚合",
|
||||
"methodology": "外显使用已发布 sw_daily 或免费申万实时;内核优先使用当日成分日线,不调用 rt_sw_k",
|
||||
}
|
||||
|
||||
def _sw_sector_members(
|
||||
@@ -508,29 +550,72 @@ class ShenwanIndustryMixin:
|
||||
codes: list[str],
|
||||
trade_date: str,
|
||||
) -> tuple[list[dict[str, Any]], str]:
|
||||
if not codes:
|
||||
wanted = [str(code).strip() for code in codes if str(code or "").strip()]
|
||||
if not wanted:
|
||||
return [], "unavailable"
|
||||
best_rows: list[dict[str, Any]] = []
|
||||
best_source = "unavailable"
|
||||
|
||||
def consider(rows: list[dict[str, Any]] | None, source: str) -> list[dict[str, Any]]:
|
||||
nonlocal best_rows, best_source
|
||||
filtered = _filter_quotes_for_codes(rows, wanted)
|
||||
if len(filtered) > len(best_rows):
|
||||
best_rows = filtered
|
||||
best_source = source
|
||||
return filtered
|
||||
|
||||
hub_market = getattr(self, "try_market_quotes", None)
|
||||
if callable(hub_market):
|
||||
filtered = consider(hub_market(trade_date) or [], "datahub")
|
||||
if len(filtered) >= max(1, int(len(wanted) * 0.9)):
|
||||
return filtered, "datahub"
|
||||
|
||||
hub = getattr(self, "try_quotes", None)
|
||||
if callable(hub):
|
||||
rows = hub(codes) or []
|
||||
if rows:
|
||||
return list(rows), "datahub"
|
||||
collected: list[dict[str, Any]] = []
|
||||
for index in range(0, len(wanted), _QUOTE_BATCH):
|
||||
collected.extend(hub(wanted[index:index + _QUOTE_BATCH]) or [])
|
||||
filtered = consider(collected, "datahub")
|
||||
if len(filtered) >= max(1, int(len(wanted) * 0.9)):
|
||||
return filtered, "datahub"
|
||||
|
||||
aggregator = getattr(self, "realtime_aggregator", None)
|
||||
loader = getattr(aggregator, "eastmoney_stock_quotes", None) if aggregator else None
|
||||
if callable(loader):
|
||||
try:
|
||||
filtered = consider(loader(wanted, expected_date=trade_date) or [], "eastmoney_ulist")
|
||||
if len(filtered) >= max(1, int(len(wanted) * 0.9)):
|
||||
return filtered, "eastmoney_ulist"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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
|
||||
quotes, source = self._free_realtime_quotes(trade_date, "")
|
||||
consider(quotes, source)
|
||||
except TushareError:
|
||||
pass
|
||||
|
||||
if best_rows:
|
||||
return best_rows, best_source
|
||||
return [], "unavailable"
|
||||
|
||||
|
||||
_QUOTE_BATCH = 60
|
||||
|
||||
|
||||
def _filter_quotes_for_codes(
|
||||
rows: list[dict[str, Any]] | None,
|
||||
codes: list[str],
|
||||
) -> list[dict[str, Any]]:
|
||||
wanted = {str(code) for code in codes if code}
|
||||
filtered: list[dict[str, Any]] = []
|
||||
seen: set[str] = set()
|
||||
for row in rows or []:
|
||||
ts_code = str(row.get("ts_code") or "")
|
||||
if ts_code in wanted and ts_code not in seen:
|
||||
seen.add(ts_code)
|
||||
filtered.append(row)
|
||||
return filtered
|
||||
|
||||
|
||||
def _filter_members_by_listing(
|
||||
|
||||
Reference in New Issue
Block a user