from __future__ import annotations from datetime import datetime from typing import Any from backend.bootstrap.config import normalize_date from backend.data.providers.tushare_client import TushareError class DragonTigerServiceMixin: def get_hot_money_profiles(self, force: bool = False) -> dict[str, Any]: cache_kind = "hot_money_profiles_v1" cache_key = "directory" cached = self.database.get_data_snapshot(cache_kind, cache_key) if cached and not force: cached["meta"] = {**cached.get("meta", {}), "cached": True} return cached if self.configured: try: payload = self._tushare_client().hot_money_profiles() except TushareError: if cached: cached["meta"] = { **cached.get("meta", {}), "cached": True, "stale": True, "notice": "名录暂未完成更新,当前展示最近一次收录结果。", } return cached return { "meta": { "source": "unavailable", "status": "unavailable", "schema_version": 1, "cached": False, "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "notice": "游资名录暂不可用,请稍后重试。", }, "summary": { "profile_count": 0, "described_count": 0, "organization_count": 0, }, "profiles": [], } payload["meta"]["cached"] = False if payload.get("meta", {}).get("status") == "success": self.database.save_data_snapshot(cache_kind, cache_key, "tushare", payload) return payload if cached: cached["meta"] = {**cached.get("meta", {}), "cached": True} return cached return { "meta": { "source": "unavailable", "status": "unavailable", "schema_version": 1, "cached": False, "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "notice": "游资名录暂不可用,请联系管理员检查行情配置。", }, "summary": { "profile_count": 0, "described_count": 0, "organization_count": 0, }, "profiles": [], } def get_dragon_tiger(self, trade_date: str, force: bool = False) -> dict[str, Any]: normalized_date = normalize_date(trade_date) cache_kind = "hot_money_detail_v3" if not force: cached = self.database.get_data_snapshot(cache_kind, normalized_date) if ( cached and cached.get("meta", {}).get("source") == "tushare" and cached.get("meta", {}).get("status") == "success" and int(cached.get("meta", {}).get("schema_version") or 0) == 3 ): cached["meta"] = {**cached.get("meta", {}), "cached": True} return cached if self.configured: try: payload = self._tushare_client().dragon_tiger(normalized_date) except TushareError as exc: return { "meta": { "requested_date": f"{normalized_date[:4]}-{normalized_date[4:6]}-{normalized_date[6:8]}", "trade_date": f"{normalized_date[:4]}-{normalized_date[4:6]}-{normalized_date[6:8]}", "source": "tushare_error", "status": "error", "schema_version": 3, "cached": False, "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "notice": "龙虎榜数据暂不可用,请稍后重试。", }, "summary": { "trader_count": 0, "identity_count": 0, "operation_count": 0, "active_stock_count": 0, "seat_net_buy_million": 0, "unclassified_count": 0, "directory_count": 0, }, "traders": [], "unclassified_seats": [], "rows": [], } payload["meta"]["cached"] = False if payload.get("meta", {}).get("status") == "success": self.database.save_data_snapshot(cache_kind, normalized_date, "tushare", payload) return payload return { "meta": { "requested_date": f"{normalized_date[:4]}-{normalized_date[4:6]}-{normalized_date[6:8]}", "trade_date": f"{normalized_date[:4]}-{normalized_date[4:6]}-{normalized_date[6:8]}", "source": "unavailable", "status": "unavailable", "schema_version": 3, "cached": False, "notice": "龙虎榜数据暂不可用,请联系管理员检查行情配置。", }, "summary": { "trader_count": 0, "identity_count": 0, "operation_count": 0, "active_stock_count": 0, "seat_net_buy_million": 0, "unclassified_count": 0, "directory_count": 0, }, "traders": [], "unclassified_seats": [], "rows": [], } def _apply_seat_aliases(self, payload: dict[str, Any]) -> dict[str, Any]: aliases = self.database.list_seat_aliases() result = dict(payload) rows = payload.get("rows") or [] for row in rows: for institution in row.get("institutions") or []: institution["alias"] = aliases.get(institution.get("seat_name", ""), "") traders: dict[tuple[str, str], dict[str, Any]] = {} unclassified: dict[str, dict[str, Any]] = {} seen_operations: set[tuple[Any, ...]] = set() builtin_aliases = { "国泰海通证券股份有限公司南京太平南路证券营业部": "作手新一", } for row in rows: for institution in row.get("institutions") or []: seat_name = str(institution.get("seat_name") or "未知席位").strip() saved_alias = str(institution.get("alias") or "").strip() builtin_alias = builtin_aliases.get(seat_name, "") if saved_alias or builtin_alias: identity_name = saved_alias or builtin_alias identity_type = "trader" recognized = True identity_source = "manual" if saved_alias else "builtin" elif "机构专用" in seat_name: identity_name = "机构专用" identity_type = "institution" recognized = True identity_source = "system" elif "沪股通专用" in seat_name or "深股通专用" in seat_name: identity_name = "北向资金" identity_type = "channel" recognized = True identity_source = "system" else: identity_name = seat_name identity_type = "unclassified" recognized = False identity_source = "raw" buy = round(float(institution.get("buy_million") or 0), 2) sell = round(float(institution.get("sell_million") or 0), 2) net_buy = round(float(institution.get("net_buy_million") or 0), 2) operation_key = (row.get("code"), seat_name, buy, sell, net_buy) if operation_key in seen_operations: continue seen_operations.add(operation_key) group_key = (identity_type, identity_name) group = traders.setdefault( group_key, { "name": identity_name, "identity_type": identity_type, "identity_source": identity_source, "recognized": recognized, "buy_million": 0.0, "sell_million": 0.0, "net_buy_million": 0.0, "seat_names": set(), "stock_codes": set(), "operations": [], }, ) group["buy_million"] += buy group["sell_million"] += sell group["net_buy_million"] += net_buy group["seat_names"].add(seat_name) group["stock_codes"].add(str(row.get("code") or "")) group["operations"].append( { "code": row.get("code") or "", "name": row.get("name") or "--", "change": row.get("change") or 0, "direction": "买入" if net_buy > 0 else "卖出" if net_buy < 0 else "持平", "buy_million": buy, "sell_million": sell, "net_buy_million": net_buy, "reason": row.get("reason") or "--", "seat_name": seat_name, "seat_alias": identity_name if recognized else "", } ) if not recognized: pending = unclassified.setdefault( seat_name, { "seat_name": seat_name, "stock_codes": set(), "operation_count": 0, "buy_million": 0.0, "sell_million": 0.0, "net_buy_million": 0.0, }, ) pending["stock_codes"].add(str(row.get("code") or "")) pending["operation_count"] += 1 pending["buy_million"] += buy pending["sell_million"] += sell pending["net_buy_million"] += net_buy type_order = {"trader": 0, "institution": 1, "channel": 2, "unclassified": 3} aggregated = list(traders.values()) aggregated.sort( key=lambda item: ( type_order.get(item["identity_type"], 9), -abs(item["net_buy_million"]), item["name"], ) ) for index, group in enumerate(aggregated, start=1): group["id"] = f"identity-{index}" group["buy_million"] = round(group["buy_million"], 2) group["sell_million"] = round(group["sell_million"], 2) group["net_buy_million"] = round(group["net_buy_million"], 2) group["seat_count"] = len(group.pop("seat_names")) group["stock_count"] = len(group.pop("stock_codes")) group["operation_count"] = len(group["operations"]) group["operations"].sort( key=lambda item: abs(float(item.get("net_buy_million") or 0)), reverse=True ) pending_seats = list(unclassified.values()) for pending in pending_seats: pending["stock_count"] = len(pending.pop("stock_codes")) pending["buy_million"] = round(pending["buy_million"], 2) pending["sell_million"] = round(pending["sell_million"], 2) pending["net_buy_million"] = round(pending["net_buy_million"], 2) pending_seats.sort(key=lambda item: abs(item["net_buy_million"]), reverse=True) operation_count = sum(item["operation_count"] for item in aggregated) active_stocks = { operation["code"] for item in aggregated for operation in item["operations"] } seat_net_buy = round(sum(item["net_buy_million"] for item in aggregated), 2) result["rows"] = rows result["traders"] = aggregated result["unclassified_seats"] = pending_seats result["summary"] = { **(payload.get("summary") or {}), "trader_count": sum(item["identity_type"] == "trader" for item in aggregated), "identity_count": len(aggregated), "operation_count": operation_count, "active_stock_count": len(active_stocks), "seat_net_buy_million": seat_net_buy, "unclassified_count": len(pending_seats), } return result