refactor: establish standalone application boundary
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from backend.data.numbers import non_nan_number as _number
|
||||
from backend.data.providers.tushare_client import TushareError
|
||||
from backend.features.market.insights_context import _display_date
|
||||
|
||||
|
||||
class MarketPopularityInsightsMixin:
|
||||
def popularity(self, requested_date: str, force: bool = False) -> dict[str, Any]:
|
||||
trade_date, previous_date = self._trade_context(requested_date)
|
||||
if not force:
|
||||
cached = self.database.get_data_snapshot("popularity_v1", trade_date)
|
||||
if cached:
|
||||
result = copy.deepcopy(cached)
|
||||
result["meta"] = {**result.get("meta", {}), "cached": True}
|
||||
return result
|
||||
|
||||
ths_rows, dc_rows, errors = self._hot_rows(trade_date)
|
||||
actual_date = trade_date
|
||||
carried_forward = False
|
||||
if not ths_rows and not dc_rows and previous_date:
|
||||
ths_rows, dc_rows, errors = self._hot_rows(previous_date)
|
||||
actual_date = previous_date
|
||||
carried_forward = bool(ths_rows or dc_rows)
|
||||
if not ths_rows and not dc_rows:
|
||||
fallback = self._latest_feature_snapshot("popularity_v1", trade_date)
|
||||
if fallback:
|
||||
result = copy.deepcopy(fallback)
|
||||
result["meta"] = {
|
||||
**result.get("meta", {}),
|
||||
"requested_date": _display_date(requested_date),
|
||||
"carried_forward": True,
|
||||
"cached": True,
|
||||
"notice": "当前榜单暂不可用,展示最近有效快照",
|
||||
}
|
||||
return result
|
||||
return {
|
||||
"meta": {
|
||||
"requested_date": _display_date(requested_date),
|
||||
"trade_date": _display_date(trade_date),
|
||||
"previous_trade_date": _display_date(previous_date),
|
||||
"carried_forward": False,
|
||||
"cached": False,
|
||||
"notice": "该交易日暂无可用人气榜",
|
||||
"updated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
},
|
||||
"summary": {"ths_count": 0, "dc_count": 0, "dual_count": 0},
|
||||
"combined": [], "ths": [], "dc": [],
|
||||
}
|
||||
|
||||
prior_request = (datetime.strptime(actual_date, "%Y%m%d") - timedelta(days=1)).strftime("%Y%m%d")
|
||||
prior_date, _ = self._trade_context(prior_request)
|
||||
previous_ths, previous_dc, _ = self._hot_rows(prior_date)
|
||||
ths = self._normalize_hot(ths_rows, "热股", previous_ths)
|
||||
dc = self._normalize_hot(dc_rows, "A股市场", previous_dc)
|
||||
ths_map = {item["ts_code"]: item for item in ths}
|
||||
dc_map = {item["ts_code"]: item for item in dc}
|
||||
combined = []
|
||||
for ts_code in set(ths_map) | set(dc_map):
|
||||
ths_item = ths_map.get(ts_code)
|
||||
dc_item = dc_map.get(ts_code)
|
||||
base = ths_item or dc_item or {}
|
||||
ths_rank = int(ths_item["rank"]) if ths_item else None
|
||||
dc_rank = int(dc_item["rank"]) if dc_item else None
|
||||
score = (
|
||||
(101 - (ths_rank or 101)) * 0.5
|
||||
+ (201 - (dc_rank or 201)) * 0.25
|
||||
)
|
||||
combined.append(
|
||||
{
|
||||
**base,
|
||||
"ths_rank": ths_rank,
|
||||
"dc_rank": dc_rank,
|
||||
"score": round(score, 2),
|
||||
"dual_source": bool(ths_item and dc_item),
|
||||
"concepts": (ths_item or {}).get("concepts") or [],
|
||||
}
|
||||
)
|
||||
combined.sort(key=lambda item: (item["dual_source"], item["score"]), reverse=True)
|
||||
for index, item in enumerate(combined, 1):
|
||||
item["rank"] = index
|
||||
result = {
|
||||
"meta": {
|
||||
"requested_date": _display_date(requested_date),
|
||||
"trade_date": _display_date(actual_date),
|
||||
"previous_trade_date": _display_date(prior_date),
|
||||
"carried_forward": carried_forward,
|
||||
"cached": False,
|
||||
"updated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
"notice": ";".join(errors),
|
||||
},
|
||||
"summary": {
|
||||
"ths_count": len(ths),
|
||||
"dc_count": len(dc),
|
||||
"dual_count": sum(item["dual_source"] for item in combined),
|
||||
},
|
||||
"combined": combined[:200],
|
||||
"ths": ths,
|
||||
"dc": dc,
|
||||
}
|
||||
self.database.save_data_snapshot("popularity_v1", trade_date, "market", result)
|
||||
return result
|
||||
|
||||
def _hot_rows(self, trade_date: str) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[str]]:
|
||||
errors = []
|
||||
try:
|
||||
ths = self.client.query("ths_hot", {"trade_date": trade_date})
|
||||
except TushareError:
|
||||
ths = []
|
||||
errors.append("同花顺榜单暂不可用")
|
||||
try:
|
||||
dc = self.client.query("dc_hot", {"trade_date": trade_date})
|
||||
except TushareError:
|
||||
dc = []
|
||||
errors.append("东方财富榜单暂不可用")
|
||||
return ths, dc, errors
|
||||
|
||||
def _normalize_hot(
|
||||
self,
|
||||
rows: list[dict[str, Any]],
|
||||
data_type: str,
|
||||
previous_rows: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
previous = {
|
||||
str(row.get("ts_code") or ""): int(_number(row.get("rank")))
|
||||
for row in previous_rows
|
||||
if str(row.get("data_type") or "") == data_type
|
||||
}
|
||||
items = []
|
||||
for row in rows:
|
||||
if str(row.get("data_type") or "") != data_type:
|
||||
continue
|
||||
rank = int(_number(row.get("rank")))
|
||||
ts_code = str(row.get("ts_code") or "")
|
||||
prior_rank = previous.get(ts_code)
|
||||
items.append(
|
||||
{
|
||||
"rank": rank,
|
||||
"ts_code": ts_code,
|
||||
"code": ts_code.split(".")[0],
|
||||
"name": str(row.get("ts_name") or "--"),
|
||||
"change": round(_number(row.get("pct_change")), 2),
|
||||
"price": round(_number(row.get("current_price")), 2),
|
||||
"hot": round(_number(row.get("hot")), 1),
|
||||
"rank_change": (prior_rank - rank) if prior_rank else None,
|
||||
"concepts": self._parse_concepts(row.get("concept")),
|
||||
"reason": str(row.get("rank_reason") or ""),
|
||||
"rank_time": str(row.get("rank_time") or ""),
|
||||
}
|
||||
)
|
||||
items.sort(key=lambda item: item["rank"])
|
||||
return items
|
||||
Reference in New Issue
Block a user