from __future__ import annotations import json from typing import Any def build_popularity( trade_date: str, ths_rows: tuple[dict[str, Any], ...], dc_rows: tuple[dict[str, Any], ...], previous_ths: tuple[dict[str, Any], ...], previous_dc: tuple[dict[str, Any], ...], ) -> dict[str, Any]: ths = _normalize(ths_rows, "热股", previous_ths) dc = _normalize(dc_rows, "A股市场", previous_dc) ths_map = {str(item["identifier"]): item for item in ths} dc_map = {str(item["identifier"]): item for item in dc} combined = [] for identifier in set(ths_map) | set(dc_map): ths_item = ths_map.get(identifier) dc_item = dc_map.get(identifier) 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": list((ths_item or {}).get("concepts") or []), } ) combined.sort( key=lambda item: (bool(item["dual_source"]), float(item["score"])), reverse=True ) for rank, item in enumerate(combined, start=1): item["rank"] = rank return { "trade_date": trade_date, "summary": { "ths_count": len(ths), "dc_count": len(dc), "dual_count": sum(bool(item["dual_source"]) for item in combined), }, "combined": combined[:200], "ths": ths, "dc": dc, } def _normalize( rows: tuple[dict[str, Any], ...], data_type: str, previous_rows: tuple[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 identifier = str(row.get("ts_code") or "") rank = int(_number(row.get("rank"))) if not identifier or rank <= 0: continue prior = previous.get(identifier) items.append( { "rank": rank, "identifier": identifier, "code": identifier.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 if prior else None, "concepts": _concepts(row.get("concept")), "reason": str(row.get("rank_reason") or ""), "rank_time": str(row.get("rank_time") or ""), } ) return sorted(items, key=lambda item: int(item["rank"])) def _concepts(value: Any) -> list[str]: if isinstance(value, list): return [str(item).strip() for item in value if str(item).strip()] text = str(value or "").strip() if not text: return [] try: parsed = json.loads(text) if isinstance(parsed, list): return [str(item).strip() for item in parsed if str(item).strip()] except json.JSONDecodeError: pass return [part.strip() for part in text.replace(",", ",").split(",") if part.strip()] def _number(value: Any) -> float: try: number = float(value) return number if number == number else 0.0 except (TypeError, ValueError): return 0.0