from __future__ import annotations import json import re from datetime import datetime from typing import Any from backend.bootstrap.config import display_compact_date as _display_date from backend.data.numbers import finite_number as _number from backend.data.providers.tushare_helpers import _text from backend.data.providers.tushare_transport import TushareError class DragonTigerMixin: def hot_money_profiles(self) -> dict[str, Any]: rows = self.query("hm_list", {}, "name,desc,orgs") profiles: list[dict[str, Any]] = [] seen_names: set[str] = set() for row in rows: name = str(row.get("name") or "").strip() if not name or name in seen_names: continue seen_names.add(name) description = _text(row.get("desc")) organization_text = _text(row.get("orgs")) parsed_organizations: Any = None if organization_text.startswith("["): try: parsed_organizations = json.loads(organization_text) except json.JSONDecodeError: parsed_organizations = None organization_parts = ( parsed_organizations if isinstance(parsed_organizations, list) else re.split(r"[,,;;\n]+", organization_text) ) organizations = list(dict.fromkeys( _text(part) for part in organization_parts if _text(part) )) profiles.append( { "id": f"hot-money-profile-{len(profiles) + 1}", "name": name, "description": description, "organizations": organizations, "organization_count": len(organizations), } ) return { "meta": { "source": "tushare", "status": "success" if profiles else "empty", "schema_version": 1, "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "notice": "", }, "summary": { "profile_count": len(profiles), "described_count": sum(bool(item["description"]) for item in profiles), "organization_count": sum(item["organization_count"] for item in profiles), }, "profiles": profiles, } def dragon_tiger(self, requested_date: str) -> dict[str, Any]: trade_date, _ = self.resolve_trade_context(requested_date) detail_rows = self.query( "hm_detail", {"trade_date": trade_date}, "trade_date,ts_code,ts_name,buy_amount,sell_amount,net_amount," "hm_name,hm_orgs,tag", ) notices: list[str] = [] try: directory_rows = self.query("hm_list", {}, "name,desc,orgs") except TushareError as exc: directory_rows = [] notices.append(f"游资名录暂不可用:{exc}") directory = { str(row.get("name") or "").strip(): { "description": _text(row.get("desc")), "orgs": _text(row.get("orgs")), } for row in directory_rows if str(row.get("name") or "").strip() } # 个股龙虎榜仅用于补充涨幅和上榜原因,不参与游资身份识别。 try: top_rows = self.query( "top_list", {"trade_date": trade_date}, "trade_date,ts_code,name,pct_change,reason", ) except TushareError as exc: top_rows = [] notices.append(f"个股龙虎榜辅助信息暂不可用:{exc}") stock_context: dict[str, dict[str, Any]] = {} for row in top_rows: ts_code = str(row.get("ts_code") or "") if ts_code and ts_code not in stock_context: stock_context[ts_code] = row groups: dict[str, dict[str, Any]] = {} for row in detail_rows: trader_name = str(row.get("hm_name") or "未命名游资").strip() ts_code = str(row.get("ts_code") or "").strip() stock = stock_context.get(ts_code, {}) directory_item = directory.get(trader_name, {}) seat_name = _text(row.get("hm_orgs")) or directory_item.get("orgs") or "--" buy = round(_number(row.get("buy_amount")) / 1000000, 2) sell = round(_number(row.get("sell_amount")) / 1000000, 2) net_buy = round(_number(row.get("net_amount")) / 1000000, 2) group = groups.setdefault( trader_name, { "name": trader_name, "description": directory_item.get("description") or "", "directory_orgs": directory_item.get("orgs") or "", "identity_type": "trader", "identity_source": "tushare_hm", "recognized": True, "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 if seat_name != "--": group["seat_names"].add(seat_name) code = ts_code.split(".")[0] if code: group["stock_codes"].add(code) group["operations"].append( { "code": code, "ts_code": ts_code, "name": row.get("ts_name") or stock.get("name") or "--", "change": ( _number(stock.get("pct_change")) if stock.get("pct_change") is not None else None ), "direction": "买入" if net_buy > 0 else "卖出" if net_buy < 0 else "持平", "buy_million": buy, "sell_million": sell, "net_buy_million": net_buy, "seat_name": seat_name, "seat_alias": trader_name, "tag": _text(row.get("tag")) or "--", "reason": _text(stock.get("reason")) or "--", } ) traders = list(groups.values()) traders.sort(key=lambda item: abs(item["net_buy_million"]), reverse=True) for index, group in enumerate(traders, start=1): group["id"] = f"hot-money-{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 ) operation_count = sum(item["operation_count"] for item in traders) active_stocks = { operation["code"] for item in traders for operation in item["operations"] if operation["code"] } net_buy_total = round(sum(item["net_buy_million"] for item in traders), 2) status = "success" if detail_rows else "partial" if top_rows else "empty" if not detail_rows: notices.insert( 0, f"当日有 {len(stock_context)} 只股票上榜,但未返回可识别的游资每日明细。" if top_rows else "该交易日未返回龙虎榜或游资每日明细。", ) return { "meta": { "requested_date": _display_date(requested_date), "trade_date": _display_date(trade_date), "source": "tushare", "status": status, "schema_version": 3, "updated_at": datetime.now().astimezone().isoformat(timespec="seconds"), "notice": ";".join(notices), }, "summary": { "trader_count": len(traders), "identity_count": len(traders), "operation_count": operation_count, "active_stock_count": len(active_stocks), "seat_net_buy_million": net_buy_total, "unclassified_count": 0, "directory_count": len(directory), "official_stock_count": len(stock_context), }, "traders": traders, "unclassified_seats": [], "rows": [], }