Files
xiaobaifupan/next/backend/features/market/insights/dragon.py
T

303 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)