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)