215 lines
8.8 KiB
Python
215 lines
8.8 KiB
Python
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": [],
|
||
}
|