migration: preserve market insights slice
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
from .repository import DragonTigerRepositoryMixin
|
||||
from .service import DragonTigerServiceMixin
|
||||
|
||||
__all__ = ["DragonTigerRepositoryMixin", "DragonTigerServiceMixin"]
|
||||
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
|
||||
class DragonTigerRepositoryMixin:
|
||||
def list_seat_aliases(self) -> dict[str, str]:
|
||||
with self.connect() as connection:
|
||||
rows = connection.execute("SELECT seat_name, alias FROM seat_aliases").fetchall()
|
||||
return {row["seat_name"]: row["alias"] for row in rows}
|
||||
|
||||
def save_seat_alias(self, seat_name: str, alias: str) -> None:
|
||||
now = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||
with self.connect() as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO seat_aliases (seat_name, alias, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(seat_name) DO UPDATE SET
|
||||
alias = excluded.alias,
|
||||
updated_at = excluded.updated_at
|
||||
""",
|
||||
(seat_name, alias, now),
|
||||
)
|
||||
|
||||
def upsert_lhb_institutions(self, rows: list[dict[str, Any]]) -> int:
|
||||
grouped: dict[tuple[str, str], dict[str, float | int]] = {}
|
||||
for row in rows:
|
||||
trade_date = str(row.get("trade_date") or "")
|
||||
ts_code = str(row.get("ts_code") or "")
|
||||
seat_name = str(row.get("exalter") or row.get("seat_name") or "")
|
||||
if not trade_date or not ts_code or "机构专用" not in seat_name:
|
||||
continue
|
||||
group = grouped.setdefault(
|
||||
(trade_date, ts_code),
|
||||
{"net": 0.0, "buy": 0.0, "sell": 0.0, "seats": 0},
|
||||
)
|
||||
group["net"] = float(group["net"]) + float(row.get("net_buy") or row.get("net_amount") or 0)
|
||||
group["buy"] = float(group["buy"]) + float(row.get("buy") or row.get("buy_amount") or 0)
|
||||
group["sell"] = float(group["sell"]) + float(row.get("sell") or row.get("sell_amount") or 0)
|
||||
group["seats"] = int(group["seats"]) + 1
|
||||
values = [
|
||||
(trade_date, ts_code, item["net"], item["buy"], item["sell"], item["seats"])
|
||||
for (trade_date, ts_code), item in grouped.items()
|
||||
]
|
||||
with self.connect() as connection:
|
||||
connection.executemany(
|
||||
"""
|
||||
INSERT INTO lhb_institution_daily
|
||||
(trade_date, ts_code, net_buy_amount, buy_amount, sell_amount, seat_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(trade_date, ts_code) DO UPDATE SET
|
||||
net_buy_amount=excluded.net_buy_amount,
|
||||
buy_amount=excluded.buy_amount,
|
||||
sell_amount=excluded.sell_amount,
|
||||
seat_count=excluded.seat_count
|
||||
""",
|
||||
values,
|
||||
)
|
||||
return len(values)
|
||||
@@ -0,0 +1,288 @@
|
||||
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
|
||||
Reference in New Issue
Block a user