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,302 @@
from __future__ import annotations
import json
import re
from typing import Any
def profiles(rows: tuple[dict[str, Any], ...]) -> list[dict[str, Any]]:
result = []
seen = set()
for row in rows:
name = _text(row.get("name"))
if not name or name in seen:
continue
seen.add(name)
organizations = _organizations(row.get("orgs"))
result.append(
{
"name": name,
"description": _text(row.get("desc")),
"organizations": organizations,
"organization_count": len(organizations),
}
)
return result
def build_dragon_list(
*,
trade_date: str,
official_rows: tuple[dict[str, Any], ...] | None,
profile_rows: tuple[dict[str, Any], ...] | None,
stock_rows: tuple[dict[str, Any], ...] | None,
seat_rows: tuple[dict[str, Any], ...] | None,
aliases: dict[str, str],
) -> dict[str, Any]:
profile_items = profiles(profile_rows or ())
profile_map = {str(item["name"]): item for item in profile_items}
organization_map = {
organization: str(item["name"])
for item in profile_items
for organization in item["organizations"]
}
stocks = _stock_context(stock_rows or ())
operations = _official_operations(official_rows or (), profile_map, stocks)
official_keys = {
(str(item["identifier"]), str(item["seat_name"]), round(float(item["net_million"]), 2))
for item in operations
}
for row in seat_rows or ():
operation = _seat_operation(row, stocks, aliases, organization_map)
key = (
str(operation["identifier"]),
str(operation["seat_name"]),
round(float(operation["net_million"]), 2),
)
if key not in official_keys:
operations.append(operation)
traders = _aggregate_traders(operations, profile_map)
unclassified = _aggregate_unclassified(operations)
official_stock_count = len(stocks)
detail_available = official_rows is not None or seat_rows is not None
detail_count = len(official_rows or ()) + len(seat_rows or ())
recognized_count = sum(bool(item["recognized"]) for item in operations)
if official_rows is None and stock_rows is None and seat_rows is None:
status = "unavailable"
message = "龙虎榜数据请求失败,请稍后重新检查"
elif official_stock_count == 0 and detail_count == 0:
status = "empty"
message = "该交易日没有股票上榜"
elif official_stock_count > 0 and (not detail_available or detail_count == 0):
status = "detail_missing"
message = f"当日有 {official_stock_count} 只股票上榜,但席位明细尚未返回"
elif detail_count > 0 and recognized_count == 0:
status = "unclassified"
message = f"当日有 {official_stock_count} 只股票上榜,席位均待归类"
else:
status = "success" if not unclassified else "partial"
message = "部分营业部尚未归类" if unclassified else ""
return {
"trade_date": trade_date,
"status": status,
"message": message,
"summary": {
"official_stock_count": official_stock_count,
"trader_count": len(traders),
"operation_count": len(operations),
"unclassified_count": len(unclassified),
"net_million": round(
sum(float(item["net_million"]) for item in operations), 2
),
"profile_count": len(profile_items),
},
"traders": traders,
"operations": sorted(
operations, key=lambda item: abs(float(item["net_million"])), reverse=True
),
"unclassified_seats": unclassified,
"profiles": profile_items,
}
def _stock_context(rows: tuple[dict[str, Any], ...]) -> dict[str, dict[str, Any]]:
result = {}
for row in rows:
identifier = str(row.get("ts_code") or "")
if identifier and identifier not in result:
result[identifier] = {
"name": _text(row.get("name")),
"change": _optional_number(row.get("pct_change")),
"reason": _text(row.get("reason")),
}
return result
def _official_operations(
rows: tuple[dict[str, Any], ...],
profile_map: dict[str, dict[str, Any]],
stocks: dict[str, dict[str, Any]],
) -> list[dict[str, Any]]:
result = []
for row in rows:
identifier = str(row.get("ts_code") or "")
trader = _text(row.get("hm_name")) or "未命名游资"
profile = profile_map.get(trader) or {}
seat = _text(row.get("hm_orgs")) or ""
stock = stocks.get(identifier) or {}
result.append(
_operation(
identifier=identifier,
name=_text(row.get("ts_name")) or str(stock.get("name") or ""),
change=stock.get("change"),
reason=str(stock.get("reason") or ""),
seat_name=seat or "未提供营业部",
trader_name=trader,
description=str(profile.get("description") or ""),
buy=_number(row.get("buy_amount")) / 1_000_000,
sell=_number(row.get("sell_amount")) / 1_000_000,
net=_number(row.get("net_amount")) / 1_000_000,
recognized=True,
)
)
return result
def _seat_operation(
row: dict[str, Any],
stocks: dict[str, dict[str, Any]],
aliases: dict[str, str],
organization_map: dict[str, str],
) -> dict[str, Any]:
identifier = str(row.get("ts_code") or "")
seat = _text(row.get("exalter")) or "未命名营业部"
trader = aliases.get(seat) or organization_map.get(seat) or ""
stock = stocks.get(identifier) or {}
buy = _number(row.get("buy")) / 1_000_000
sell = _number(row.get("sell")) / 1_000_000
net = _number(row.get("net_buy")) / 1_000_000
if net == 0 and (buy or sell):
net = buy - sell
return _operation(
identifier=identifier,
name=str(stock.get("name") or ""),
change=stock.get("change"),
reason=_text(row.get("reason")) or str(stock.get("reason") or ""),
seat_name=seat,
trader_name=trader,
description="",
buy=buy,
sell=sell,
net=net,
recognized=bool(trader),
)
def _operation(
*,
identifier: str,
name: str,
change: float | None,
reason: str,
seat_name: str,
trader_name: str,
description: str,
buy: float,
sell: float,
net: float,
recognized: bool,
) -> dict[str, Any]:
return {
"identifier": identifier,
"code": identifier.split(".")[0],
"name": name,
"change": change,
"direction": "买入" if net > 0 else "卖出" if net < 0 else "持平",
"buy_million": round(buy, 2),
"sell_million": round(sell, 2),
"net_million": round(net, 2),
"seat_name": seat_name,
"trader_name": trader_name,
"description": description,
"reason": reason,
"recognized": recognized,
}
def _aggregate_traders(
operations: list[dict[str, Any]], profile_map: dict[str, dict[str, Any]]
) -> list[dict[str, Any]]:
groups: dict[str, dict[str, Any]] = {}
for operation in operations:
name = str(operation.get("trader_name") or "")
if not operation.get("recognized") or not name:
continue
group = groups.setdefault(
name,
{
"name": name,
"description": str((profile_map.get(name) or {}).get("description") or ""),
"buy_million": 0.0,
"sell_million": 0.0,
"net_million": 0.0,
"seats": set(),
"stocks": set(),
"operations": [],
},
)
group["buy_million"] += float(operation["buy_million"])
group["sell_million"] += float(operation["sell_million"])
group["net_million"] += float(operation["net_million"])
group["seats"].add(str(operation["seat_name"]))
group["stocks"].add(str(operation["code"]))
group["operations"].append(operation)
result = []
for group in groups.values():
result.append(
{
"name": group["name"],
"description": group["description"],
"buy_million": round(group["buy_million"], 2),
"sell_million": round(group["sell_million"], 2),
"net_million": round(group["net_million"], 2),
"seat_count": len(group["seats"]),
"stock_count": len(group["stocks"]),
"operation_count": len(group["operations"]),
"operations": sorted(
group["operations"],
key=lambda item: abs(float(item["net_million"])),
reverse=True,
),
}
)
return sorted(result, key=lambda item: abs(float(item["net_million"])), reverse=True)
def _aggregate_unclassified(operations: list[dict[str, Any]]) -> list[dict[str, Any]]:
groups: dict[str, dict[str, Any]] = {}
for operation in operations:
if operation.get("recognized"):
continue
seat = str(operation["seat_name"])
group = groups.setdefault(
seat, {"seat_name": seat, "net_million": 0.0, "operation_count": 0}
)
group["net_million"] += float(operation["net_million"])
group["operation_count"] += 1
result = [
{**group, "net_million": round(float(group["net_million"]), 2)}
for group in groups.values()
]
return sorted(result, key=lambda item: abs(float(item["net_million"])), reverse=True)
def _organizations(value: Any) -> list[str]:
text = _text(value)
parsed: Any = None
if text.startswith("["):
try:
parsed = json.loads(text)
except json.JSONDecodeError:
parsed = None
values = parsed if isinstance(parsed, list) else re.split(r"[,;\n]+", text)
return list(dict.fromkeys(_text(item) for item in values if _text(item)))
def _text(value: Any) -> str:
return str(value or "").strip()
def _number(value: Any) -> float:
try:
number = float(value)
return number if number == number else 0.0
except (TypeError, ValueError):
return 0.0
def _optional_number(value: Any) -> float | None:
if value in (None, ""):
return None
return _number(value)