rebuild(stage-8): deliver market insight workspaces

This commit is contained in:
leefer
2026-07-30 04:12:04 +08:00
parent a18e8e9d27
commit 976a5cac03
39 changed files with 3671 additions and 14 deletions
@@ -0,0 +1,124 @@
from __future__ import annotations
from typing import Any
def build_theme_library(
trade_date: str,
directory_rows: tuple[dict[str, Any], ...],
daily_rows: tuple[dict[str, Any], ...],
hot_rows: tuple[dict[str, Any], ...],
) -> dict[str, Any]:
daily = {str(row.get("ts_code") or ""): row for row in daily_rows}
hot = {
str(row.get("ts_code") or ""): int(_number(row.get("rank"), 9999))
for row in hot_rows
if str(row.get("data_type") or "") == "概念板块"
}
items = []
for row in directory_rows:
if str(row.get("type") or "").upper() != "N":
continue
if str(row.get("exchange") or "").upper() != "A":
continue
code = str(row.get("ts_code") or "")
name = str(row.get("name") or "").strip()
if not code or not name:
continue
quote = daily.get(code)
items.append(
{
"code": code,
"name": name,
"member_count": int(_number(row.get("count"))),
"change": round(_number((quote or {}).get("pct_change")), 2)
if quote
else None,
"close": round(_number((quote or {}).get("close")), 3) if quote else None,
"turnover_rate": round(_number((quote or {}).get("turnover_rate")), 2)
if quote
else None,
"hot_rank": hot.get(code),
"has_quote": bool(quote),
}
)
items.sort(
key=lambda item: (
bool(item["has_quote"]),
item["hot_rank"] is not None,
-(item["hot_rank"] or 9999),
_number(item["change"], -999),
),
reverse=True,
)
quoted = [item for item in items if item["has_quote"]]
return {
"trade_date": trade_date,
"summary": {
"theme_count": len(items),
"quoted_count": len(quoted),
"up_count": sum(_number(item["change"]) > 0 for item in quoted),
"down_count": sum(_number(item["change"]) < 0 for item in quoted),
"hot_count": len(hot),
},
"items": items,
}
def build_theme_detail(
trade_date: str,
theme: dict[str, Any],
member_rows: tuple[dict[str, Any], ...],
daily_rows: tuple[dict[str, Any], ...],
) -> dict[str, Any]:
daily = {str(row.get("ts_code") or ""): row for row in daily_rows}
members = []
seen = set()
for row in member_rows:
identifier = str(row.get("con_code") or "")
if not identifier or identifier in seen:
continue
seen.add(identifier)
quote = daily.get(identifier)
members.append(
{
"identifier": identifier,
"code": identifier.split(".")[0],
"name": str(row.get("con_name") or ""),
"change": round(_number((quote or {}).get("pct_chg")), 2)
if quote
else None,
"close": round(_number((quote or {}).get("close")), 2) if quote else None,
"amount": _number((quote or {}).get("amount")) * 1000 if quote else None,
"quoted": bool(quote),
}
)
members.sort(
key=lambda item: (
bool(item["quoted"]),
_number(item["change"], -999),
_number(item["amount"]),
),
reverse=True,
)
quoted = [item for item in members if item["quoted"]]
return {
"trade_date": trade_date,
"theme": theme,
"summary": {
"member_count": len(members),
"quoted_count": len(quoted),
"up_count": sum(_number(item["change"]) > 0 for item in quoted),
"down_count": sum(_number(item["change"]) < 0 for item in quoted),
"turnover_rate": theme.get("turnover_rate"),
},
"members": members,
}
def _number(value: Any, default: float = 0.0) -> float:
try:
number = float(value)
return number if number == number else default
except (TypeError, ValueError):
return default