222 lines
10 KiB
Python
222 lines
10 KiB
Python
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)
|