refactor: establish standalone application boundary
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from datetime import datetime
|
||||
from statistics import median
|
||||
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 MarketAuctionInsightsMixin:
|
||||
def auction_center(
|
||||
self,
|
||||
requested_date: str,
|
||||
force: bool = False,
|
||||
user_id: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
trade_date, previous_date = self._trade_context(requested_date)
|
||||
session = self._auction_session(requested_date, trade_date)
|
||||
phase = str(session["phase"])
|
||||
ifind_ready = bool(self.ifind and self.ifind.configured)
|
||||
live_dynamic = phase == "observing" and ifind_ready
|
||||
use_ifind_snapshot = phase in {"observing", "selection", "finalized"} and ifind_ready
|
||||
data_date = previous_date if phase == "pending" or (phase == "observing" and not live_dynamic) else trade_date
|
||||
carried_forward = data_date != trade_date
|
||||
cache_key = data_date
|
||||
if not force and not live_dynamic:
|
||||
cached = self.database.get_data_snapshot("auction_center_v6", cache_key)
|
||||
if cached:
|
||||
result = copy.deepcopy(cached)
|
||||
result["meta"] = {
|
||||
**result.get("meta", {}),
|
||||
**session,
|
||||
"requested_date": _display_date(requested_date),
|
||||
"trade_date": _display_date(data_date),
|
||||
"carried_forward": carried_forward,
|
||||
"available": bool((result.get("summary") or {}).get("stock_count")),
|
||||
"cached": True,
|
||||
}
|
||||
return self._with_auction_watchlist(result, data_date, user_id)
|
||||
|
||||
if use_ifind_snapshot:
|
||||
rows = self._dynamic_auction_rows(data_date, previous_date, user_id)
|
||||
else:
|
||||
rows = []
|
||||
if not rows and not live_dynamic:
|
||||
try:
|
||||
rows = self.client.query("stk_auction", {"trade_date": data_date})
|
||||
except TushareError:
|
||||
rows = self.database.auction_factors_for_date(data_date)
|
||||
if not rows:
|
||||
return {
|
||||
"meta": {
|
||||
**session,
|
||||
"requested_date": _display_date(requested_date),
|
||||
"trade_date": _display_date(data_date),
|
||||
"carried_forward": carried_forward,
|
||||
"available": False,
|
||||
"cached": False,
|
||||
"notice": "该交易日暂无可用竞价快照",
|
||||
"updated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
},
|
||||
"summary": {
|
||||
"stock_count": 0, "up_count": 0, "down_count": 0,
|
||||
"limit_open_count": 0, "strong_open_count": 0,
|
||||
"median_change": 0, "amount_billion": 0,
|
||||
"candidate_count": 0, "focus_count": 0, "one_price_count": 0,
|
||||
},
|
||||
"expectations": {"超预期": 0, "符合预期": 0, "低于预期": 0},
|
||||
"candidate_meta": {"baseline_date": _display_date(previous_date)},
|
||||
"themes": {"carry": [], "new_themes": []},
|
||||
"amount_history": self._auction_amount_history(data_date),
|
||||
"news_feedback": {"available": False, "message": "隔夜消息反馈暂不可用"},
|
||||
"focus_rows": [], "one_price_rows": [], "rows": [],
|
||||
"watchlist_rows": [], "watchlist_missing_count": 0,
|
||||
}
|
||||
|
||||
master = self._stock_master()
|
||||
try:
|
||||
limit_rows = self.client.query(
|
||||
"stk_limit",
|
||||
{"trade_date": data_date},
|
||||
"trade_date,ts_code,up_limit,down_limit",
|
||||
)
|
||||
except TushareError:
|
||||
limit_rows = []
|
||||
limit_map = {str(item.get("ts_code") or ""): item for item in limit_rows}
|
||||
normalized = []
|
||||
for row in rows:
|
||||
ts_code = str(row.get("ts_code") or "")
|
||||
stock = master.get(ts_code)
|
||||
price = _number(row.get("price"))
|
||||
pre_close = _number(row.get("pre_close"))
|
||||
list_date = str((stock or {}).get("list_date") or "")
|
||||
if (
|
||||
not stock
|
||||
or price <= 0
|
||||
or pre_close <= 0
|
||||
or (list_date and list_date >= data_date)
|
||||
):
|
||||
continue
|
||||
change = (price / pre_close - 1) * 100
|
||||
amount_million = _number(row.get("amount")) / 1_000_000
|
||||
volume_ratio = _number(row.get("volume_ratio"))
|
||||
turnover_rate = _number(row.get("turnover_rate"))
|
||||
up_limit = _number((limit_map.get(ts_code) or {}).get("up_limit"))
|
||||
is_one_price = bool(
|
||||
up_limit > 0 and abs(price - up_limit) <= max(0.001, up_limit * 0.00005)
|
||||
)
|
||||
normalized.append(
|
||||
{
|
||||
"code": str(stock.get("code") or ts_code.split(".")[0]),
|
||||
"ts_code": ts_code,
|
||||
"name": str(stock.get("name") or "--"),
|
||||
"sector": str(stock.get("industry") or "其他"),
|
||||
"price": round(price, 2),
|
||||
"pre_close": round(pre_close, 2),
|
||||
"change": round(change, 2),
|
||||
"volume_ten_thousand": round(_number(row.get("vol")) / 10_000, 2),
|
||||
"amount_million": round(amount_million, 2),
|
||||
"turnover_rate": round(turnover_rate, 4),
|
||||
"volume_ratio": round(volume_ratio, 2),
|
||||
"up_limit": round(up_limit, 2) if up_limit else None,
|
||||
"is_one_price": is_one_price,
|
||||
"signal": (
|
||||
"竞价涨停" if change >= 9.5 else
|
||||
"强势高开" if change >= 3 else
|
||||
"高开" if change > 0.2 else
|
||||
"深度低开" if change <= -3 else
|
||||
"低开" if change < -0.2 else "平开"
|
||||
),
|
||||
}
|
||||
)
|
||||
normalized.sort(key=lambda item: (item["amount_million"], item["volume_ratio"]), reverse=True)
|
||||
self.database.upsert_auction_factors(rows)
|
||||
changes = [item["change"] for item in normalized]
|
||||
total = len(normalized)
|
||||
_, baseline_date = self._trade_context(data_date)
|
||||
candidates, candidate_meta, focus_rows = self._auction_candidates(normalized, baseline_date)
|
||||
candidate_map = {str(item.get("code") or ""): item for item in candidates}
|
||||
one_price_rows = []
|
||||
for row in normalized:
|
||||
if not row.get("is_one_price"):
|
||||
continue
|
||||
enriched = candidate_map.get(str(row.get("code") or ""), {})
|
||||
one_price_rows.append(
|
||||
{
|
||||
**row,
|
||||
**enriched,
|
||||
"attention_score": None,
|
||||
"expectation": "",
|
||||
"expected_change": None,
|
||||
"expectation_reason": "竞价价格封于当日涨停价,已从普通异动评分中隔离",
|
||||
}
|
||||
)
|
||||
one_price_codes = {str(item.get("code") or "") for item in one_price_rows}
|
||||
candidates = [item for item in candidates if str(item.get("code") or "") not in one_price_codes]
|
||||
focus_rows = [item for item in focus_rows if str(item.get("code") or "") not in one_price_codes]
|
||||
one_price_rows.sort(
|
||||
key=lambda item: (
|
||||
bool(item.get("is_market_core")),
|
||||
_number(item.get("prior_streak")),
|
||||
_number(item.get("amount_million")),
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
expectations = {
|
||||
label: sum(item.get("expectation") == label for item in candidates)
|
||||
for label in ("超预期", "符合预期", "低于预期")
|
||||
}
|
||||
prior_snapshot = self.database.get_snapshot(baseline_date) or {}
|
||||
themes = self._auction_theme_evidence(prior_snapshot, candidates + one_price_rows)
|
||||
self._ensure_auction_amount_history(data_date)
|
||||
amount_history = self._auction_amount_history(data_date)
|
||||
prior_amounts = [item["amount_billion"] for item in amount_history[:-1]]
|
||||
current_amount = round(sum(item["amount_million"] for item in normalized) / 100, 2)
|
||||
previous_amount = prior_amounts[-1] if prior_amounts else 0
|
||||
five_day_amounts = prior_amounts[-5:]
|
||||
five_day_average = sum(five_day_amounts) / len(five_day_amounts) if five_day_amounts else 0
|
||||
result = {
|
||||
"meta": {
|
||||
"requested_date": _display_date(requested_date),
|
||||
"trade_date": _display_date(data_date),
|
||||
"carried_forward": carried_forward,
|
||||
"available": bool(normalized),
|
||||
**session,
|
||||
"cached": False,
|
||||
"updated_at": datetime.now().astimezone().isoformat(timespec="seconds"),
|
||||
},
|
||||
"summary": {
|
||||
"stock_count": total,
|
||||
"up_count": sum(value > 0.2 for value in changes),
|
||||
"down_count": sum(value < -0.2 for value in changes),
|
||||
"limit_open_count": len(one_price_rows),
|
||||
"strong_open_count": sum(value >= 3 for value in changes),
|
||||
"median_change": round(median(changes), 2) if changes else 0,
|
||||
"amount_billion": current_amount,
|
||||
"amount_change_previous": round((current_amount / previous_amount - 1) * 100, 1) if previous_amount else None,
|
||||
"amount_change_5d": round((current_amount / five_day_average - 1) * 100, 1) if five_day_average else None,
|
||||
"candidate_count": len(candidates),
|
||||
"focus_count": len(focus_rows),
|
||||
"one_price_count": len(one_price_rows),
|
||||
},
|
||||
"expectations": expectations,
|
||||
"candidate_meta": candidate_meta,
|
||||
"themes": themes,
|
||||
"amount_history": amount_history,
|
||||
"news_feedback": {
|
||||
"available": False,
|
||||
"message": "隔夜消息反馈暂不可用",
|
||||
"detail": "待稳定的新闻与公告数据接入后开放",
|
||||
},
|
||||
"focus_rows": focus_rows,
|
||||
"one_price_rows": one_price_rows,
|
||||
"rows": candidates,
|
||||
}
|
||||
if not live_dynamic:
|
||||
self.database.save_data_snapshot("auction_center_v6", cache_key, "market", result)
|
||||
return self._with_auction_watchlist(result, data_date, user_id)
|
||||
Reference in New Issue
Block a user