from __future__ import annotations import copy from datetime import datetime, timedelta from typing import Any from backend.data.numbers import non_nan_number as _number from backend.data.providers.tushare_client import TushareError from backend.features.market.insights_context import _display_date class MarketPopularityInsightsMixin: def popularity(self, requested_date: str, force: bool = False) -> dict[str, Any]: trade_date, previous_date = self._trade_context(requested_date) if not force: cached = self.database.get_data_snapshot("popularity_v1", trade_date) if cached: result = copy.deepcopy(cached) result["meta"] = {**result.get("meta", {}), "cached": True} return result ths_rows, dc_rows, errors = self._hot_rows(trade_date) actual_date = trade_date carried_forward = False if not ths_rows and not dc_rows and previous_date: ths_rows, dc_rows, errors = self._hot_rows(previous_date) actual_date = previous_date carried_forward = bool(ths_rows or dc_rows) if not ths_rows and not dc_rows: fallback = self._latest_feature_snapshot("popularity_v1", trade_date) if fallback: result = copy.deepcopy(fallback) result["meta"] = { **result.get("meta", {}), "requested_date": _display_date(requested_date), "carried_forward": True, "cached": True, "notice": "当前榜单暂不可用,展示最近有效快照", } return result return { "meta": { "requested_date": _display_date(requested_date), "trade_date": _display_date(trade_date), "previous_trade_date": _display_date(previous_date), "carried_forward": False, "cached": False, "notice": "该交易日暂无可用人气榜", "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), }, "summary": {"ths_count": 0, "dc_count": 0, "dual_count": 0}, "combined": [], "ths": [], "dc": [], } prior_request = (datetime.strptime(actual_date, "%Y%m%d") - timedelta(days=1)).strftime("%Y%m%d") prior_date, _ = self._trade_context(prior_request) previous_ths, previous_dc, _ = self._hot_rows(prior_date) ths = self._normalize_hot(ths_rows, "热股", previous_ths) dc = self._normalize_hot(dc_rows, "A股市场", previous_dc) ths_map = {item["ts_code"]: item for item in ths} dc_map = {item["ts_code"]: item for item in dc} combined = [] for ts_code in set(ths_map) | set(dc_map): ths_item = ths_map.get(ts_code) dc_item = dc_map.get(ts_code) base = ths_item or dc_item or {} ths_rank = int(ths_item["rank"]) if ths_item else None dc_rank = int(dc_item["rank"]) if dc_item else None score = ( (101 - (ths_rank or 101)) * 0.5 + (201 - (dc_rank or 201)) * 0.25 ) combined.append( { **base, "ths_rank": ths_rank, "dc_rank": dc_rank, "score": round(score, 2), "dual_source": bool(ths_item and dc_item), "concepts": (ths_item or {}).get("concepts") or [], } ) combined.sort(key=lambda item: (item["dual_source"], item["score"]), reverse=True) for index, item in enumerate(combined, 1): item["rank"] = index result = { "meta": { "requested_date": _display_date(requested_date), "trade_date": _display_date(actual_date), "previous_trade_date": _display_date(prior_date), "carried_forward": carried_forward, "cached": False, "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "notice": ";".join(errors), }, "summary": { "ths_count": len(ths), "dc_count": len(dc), "dual_count": sum(item["dual_source"] for item in combined), }, "combined": combined[:200], "ths": ths, "dc": dc, } self.database.save_data_snapshot("popularity_v1", trade_date, "market", result) return result def _hot_rows(self, trade_date: str) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[str]]: errors = [] try: ths = self.client.query("ths_hot", {"trade_date": trade_date}) except TushareError: ths = [] errors.append("同花顺榜单暂不可用") try: dc = self.client.query("dc_hot", {"trade_date": trade_date}) except TushareError: dc = [] errors.append("东方财富榜单暂不可用") return ths, dc, errors def _normalize_hot( self, rows: list[dict[str, Any]], data_type: str, previous_rows: list[dict[str, Any]], ) -> list[dict[str, Any]]: previous = { str(row.get("ts_code") or ""): int(_number(row.get("rank"))) for row in previous_rows if str(row.get("data_type") or "") == data_type } items = [] for row in rows: if str(row.get("data_type") or "") != data_type: continue rank = int(_number(row.get("rank"))) ts_code = str(row.get("ts_code") or "") prior_rank = previous.get(ts_code) items.append( { "rank": rank, "ts_code": ts_code, "code": ts_code.split(".")[0], "name": str(row.get("ts_name") or "--"), "change": round(_number(row.get("pct_change")), 2), "price": round(_number(row.get("current_price")), 2), "hot": round(_number(row.get("hot")), 1), "rank_change": (prior_rank - rank) if prior_rank else None, "concepts": self._parse_concepts(row.get("concept")), "reason": str(row.get("rank_reason") or ""), "rank_time": str(row.get("rank_time") or ""), } ) items.sort(key=lambda item: item["rank"]) return items