rebuild(stage-8): deliver market insight workspaces
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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
|
||||
Reference in New Issue
Block a user