refactor: establish standalone application boundary
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from datetime import datetime, time as dt_time, timedelta
|
||||
from typing import Any
|
||||
|
||||
from backend.data.numbers import non_nan_number as _number
|
||||
from backend.data.providers.ifind_client import IfindError
|
||||
from backend.data.providers.tushare_client import TushareError
|
||||
from backend.features.market.insights_context import CHINA_TIMEZONE, _display_date
|
||||
|
||||
|
||||
class MarketAuctionDataMixin:
|
||||
def _auction_session(self, requested_date: str, trade_date: str) -> dict[str, Any]:
|
||||
now = self._now_provider()
|
||||
if now.tzinfo is None:
|
||||
now = now.replace(tzinfo=CHINA_TIMEZONE)
|
||||
else:
|
||||
now = now.astimezone(CHINA_TIMEZONE)
|
||||
requested = str(requested_date or "").replace("-", "")
|
||||
today = now.strftime("%Y%m%d")
|
||||
if requested != today or trade_date != today:
|
||||
return {
|
||||
"phase": "archive",
|
||||
"actionable": False,
|
||||
"next_transition_at": "",
|
||||
}
|
||||
|
||||
local_time = now.time().replace(tzinfo=None)
|
||||
transitions = (
|
||||
(dt_time(9, 15), "pending", dt_time(9, 15)),
|
||||
(dt_time(9, 25), "observing", dt_time(9, 25)),
|
||||
(dt_time(9, 30), "selection", dt_time(9, 30)),
|
||||
)
|
||||
for boundary, phase, next_boundary in transitions:
|
||||
if local_time < boundary:
|
||||
transition = now.replace(
|
||||
hour=next_boundary.hour,
|
||||
minute=next_boundary.minute,
|
||||
second=0,
|
||||
microsecond=0,
|
||||
)
|
||||
return {
|
||||
"phase": phase,
|
||||
"actionable": phase == "selection",
|
||||
"next_transition_at": transition.isoformat(timespec="seconds"),
|
||||
}
|
||||
return {
|
||||
"phase": "finalized",
|
||||
"actionable": False,
|
||||
"next_transition_at": "",
|
||||
}
|
||||
|
||||
def _auction_amount_history(self, trade_date: str) -> list[dict[str, Any]]:
|
||||
dates = self.database.auction_factor_dates(trade_date, 10)
|
||||
stock_list_dates = {
|
||||
str(item.get("ts_code") or ""): str(item.get("list_date") or "")
|
||||
for item in self.database.list_stock_master()
|
||||
if item.get("ts_code")
|
||||
}
|
||||
history = []
|
||||
for current_date in dates:
|
||||
rows = [
|
||||
row for row in self.database.auction_factors_for_date(current_date)
|
||||
if (
|
||||
str(row.get("ts_code") or "") in stock_list_dates
|
||||
and (
|
||||
not stock_list_dates[str(row.get("ts_code") or "")]
|
||||
or stock_list_dates[str(row.get("ts_code") or "")] < current_date
|
||||
)
|
||||
)
|
||||
]
|
||||
history.append(
|
||||
{
|
||||
"trade_date": _display_date(current_date),
|
||||
"amount_billion": round(sum(_number(row.get("amount")) for row in rows) / 100_000_000, 2),
|
||||
"stock_count": len(rows),
|
||||
}
|
||||
)
|
||||
return history
|
||||
|
||||
def _ensure_auction_amount_history(self, trade_date: str, target_days: int = 10) -> None:
|
||||
existing = set(self.database.auction_factor_dates(trade_date, target_days + 5))
|
||||
if len(existing) >= target_days:
|
||||
return
|
||||
end = datetime.strptime(trade_date, "%Y%m%d")
|
||||
start = (end - timedelta(days=35)).strftime("%Y%m%d")
|
||||
try:
|
||||
calendar = self.client.query(
|
||||
"trade_cal",
|
||||
{
|
||||
"exchange": "SSE",
|
||||
"start_date": start,
|
||||
"end_date": trade_date,
|
||||
"is_open": 1,
|
||||
},
|
||||
"cal_date,is_open",
|
||||
)
|
||||
except TushareError:
|
||||
return
|
||||
dates = sorted(
|
||||
str(item.get("cal_date") or "")
|
||||
for item in calendar
|
||||
if int(_number(item.get("is_open"))) == 1 and item.get("cal_date")
|
||||
)[-target_days:]
|
||||
for current_date in dates:
|
||||
if current_date in existing:
|
||||
continue
|
||||
try:
|
||||
rows = self.client.query(
|
||||
"stk_auction",
|
||||
{"trade_date": current_date},
|
||||
"ts_code,trade_date,vol,price,amount,pre_close,turnover_rate,volume_ratio,float_share",
|
||||
)
|
||||
except TushareError:
|
||||
break
|
||||
if rows:
|
||||
self.database.upsert_auction_factors(rows)
|
||||
existing.add(current_date)
|
||||
|
||||
def _with_auction_watchlist(
|
||||
self,
|
||||
result: dict[str, Any],
|
||||
trade_date: str,
|
||||
user_id: int,
|
||||
) -> dict[str, Any]:
|
||||
personalized = copy.deepcopy(result)
|
||||
if not user_id:
|
||||
personalized["watchlist_rows"] = []
|
||||
personalized["watchlist_missing_count"] = 0
|
||||
return personalized
|
||||
watched = self.database.list_watchlist(user_id)
|
||||
if not watched:
|
||||
personalized["watchlist_rows"] = []
|
||||
personalized["watchlist_missing_count"] = 0
|
||||
return personalized
|
||||
|
||||
public_rows = {
|
||||
str(item.get("code") or ""): item
|
||||
for item in (
|
||||
list(personalized.get("rows") or [])
|
||||
+ list(personalized.get("one_price_rows") or [])
|
||||
)
|
||||
}
|
||||
factors = {
|
||||
str(item.get("ts_code") or "").split(".")[0]: item
|
||||
for item in self.database.auction_factors_for_date(trade_date)
|
||||
}
|
||||
master = {
|
||||
str(item.get("ts_code") or "").split(".")[0]: item
|
||||
for item in self.database.list_stock_master()
|
||||
}
|
||||
rows = []
|
||||
missing = 0
|
||||
for item in watched:
|
||||
code = str(item.get("code") or "")
|
||||
if code in public_rows:
|
||||
rows.append({**public_rows[code], "is_watchlist": True})
|
||||
continue
|
||||
factor = factors.get(code)
|
||||
if not factor:
|
||||
missing += 1
|
||||
rows.append(
|
||||
{
|
||||
"code": code,
|
||||
"name": str(item.get("name") or "--"),
|
||||
"sector": str(item.get("sector") or "其他"),
|
||||
"available": False,
|
||||
"is_watchlist": True,
|
||||
}
|
||||
)
|
||||
continue
|
||||
stock = master.get(code, {})
|
||||
price = _number(factor.get("price"))
|
||||
pre_close = _number(factor.get("pre_close"))
|
||||
change = (price / pre_close - 1) * 100 if price > 0 and pre_close > 0 else 0
|
||||
row = {
|
||||
"code": code,
|
||||
"ts_code": str(factor.get("ts_code") or ""),
|
||||
"name": str(item.get("name") or stock.get("name") or "--"),
|
||||
"sector": str(item.get("sector") or stock.get("industry") or "其他"),
|
||||
"price": round(price, 2),
|
||||
"pre_close": round(pre_close, 2),
|
||||
"change": round(change, 2),
|
||||
"amount_million": round(_number(factor.get("amount")) / 1_000_000, 2),
|
||||
"turnover_rate": round(_number(factor.get("turnover_rate")), 4),
|
||||
"volume_ratio": round(_number(factor.get("volume_ratio")), 2),
|
||||
"candidate_sources": ["我的自选"],
|
||||
"source_label": "我的自选",
|
||||
"prior_streak": 0,
|
||||
"concepts": [],
|
||||
"expected_change": 0.0,
|
||||
"core_tags": [],
|
||||
"is_market_core": False,
|
||||
"is_watchlist": True,
|
||||
"available": True,
|
||||
}
|
||||
actual_strength = change + self._auction_confirmation(row)
|
||||
row["actual_strength"] = round(actual_strength, 2)
|
||||
row["expectation"] = self._expectation_label(actual_strength, 0.0)
|
||||
row["attention_score"] = self._attention_score(row, 0.0, [], ["我的自选"], 0, False)
|
||||
direction = "高于" if change > 0 else "低于" if change < 0 else "贴合"
|
||||
row["expectation_reason"] = f"自选观察;竞价涨幅{direction}个人观察基准{abs(change):.1f}个百分点,量比{row['volume_ratio']:.2f}"
|
||||
rows.append(row)
|
||||
rows.sort(
|
||||
key=lambda row: (bool(row.get("available", True)), _number(row.get("attention_score"))),
|
||||
reverse=True,
|
||||
)
|
||||
personalized["watchlist_rows"] = rows
|
||||
personalized["watchlist_missing_count"] = missing
|
||||
return personalized
|
||||
|
||||
def _dynamic_auction_rows(
|
||||
self,
|
||||
trade_date: str,
|
||||
baseline_date: str,
|
||||
user_id: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
if not self.ifind or not self.ifind.configured:
|
||||
return []
|
||||
master = self._stock_master()
|
||||
placeholders = [
|
||||
{
|
||||
"code": str(item.get("code") or ts_code.split(".")[0]),
|
||||
"ts_code": ts_code,
|
||||
"name": str(item.get("name") or "--"),
|
||||
"sector": str(item.get("industry") or "其他"),
|
||||
}
|
||||
for ts_code, item in master.items()
|
||||
]
|
||||
candidates, _, _ = self._auction_candidates(placeholders, baseline_date)
|
||||
selected_codes = {
|
||||
str(item.get("ts_code") or "")
|
||||
for item in candidates
|
||||
if item.get("ts_code")
|
||||
}
|
||||
if user_id:
|
||||
watched = {str(item.get("code") or "") for item in self.database.list_watchlist(user_id)}
|
||||
selected_codes.update(
|
||||
ts_code for ts_code in master if ts_code.split(".")[0] in watched
|
||||
)
|
||||
selected_codes.discard("")
|
||||
if not selected_codes:
|
||||
return []
|
||||
|
||||
display_date = _display_date(trade_date)
|
||||
now = self._now_provider()
|
||||
if now.tzinfo is None:
|
||||
now = now.replace(tzinfo=CHINA_TIMEZONE)
|
||||
else:
|
||||
now = now.astimezone(CHINA_TIMEZONE)
|
||||
end_time = min(now.time().replace(tzinfo=None), dt_time(9, 25))
|
||||
end_stamp = f"{display_date} {end_time.strftime('%H:%M:%S')}"
|
||||
start_stamp = f"{display_date} 09:15:00"
|
||||
snapshot_rows: list[dict[str, Any]] = []
|
||||
ordered_codes = sorted(selected_codes)
|
||||
for index in range(0, len(ordered_codes), 80):
|
||||
try:
|
||||
snapshot_rows.extend(
|
||||
self.ifind.snapshots(
|
||||
ordered_codes[index:index + 80],
|
||||
[
|
||||
"latest", "volume", "amount", "preClose",
|
||||
"bid1", "bidSize1", "ask1", "askSize1",
|
||||
],
|
||||
start_stamp,
|
||||
end_stamp,
|
||||
cache_ttl=8,
|
||||
)
|
||||
)
|
||||
except IfindError:
|
||||
continue
|
||||
|
||||
latest: dict[str, dict[str, Any]] = {}
|
||||
for row in snapshot_rows:
|
||||
ts_code = str(row.get("thscode") or "")
|
||||
previous = latest.get(ts_code) or {}
|
||||
if (
|
||||
ts_code
|
||||
and _number(row.get("latest")) > 0
|
||||
and str(row.get("time") or "") >= str(previous.get("time") or "")
|
||||
):
|
||||
latest[ts_code] = row
|
||||
prior_factors = {
|
||||
str(item.get("ts_code") or ""): item
|
||||
for item in self.database.auction_factors_for_date(baseline_date)
|
||||
}
|
||||
normalized = []
|
||||
for ts_code, row in latest.items():
|
||||
price = _number(row.get("latest"))
|
||||
pre_close = _number(row.get("preClose"))
|
||||
volume = _number(row.get("volume"))
|
||||
bid_size = _number(row.get("bidSize1"))
|
||||
ask_size = _number(row.get("askSize1"))
|
||||
if volume <= 0 and bid_size > 0 and ask_size > 0:
|
||||
volume = min(bid_size, ask_size)
|
||||
amount = _number(row.get("amount"))
|
||||
if amount <= 0 and price > 0 and volume > 0:
|
||||
amount = price * volume
|
||||
prior_volume = _number((prior_factors.get(ts_code) or {}).get("vol"))
|
||||
normalized.append(
|
||||
{
|
||||
"ts_code": ts_code,
|
||||
"trade_date": trade_date,
|
||||
"vol": volume,
|
||||
"price": price,
|
||||
"amount": amount,
|
||||
"pre_close": pre_close,
|
||||
"turnover_rate": 0,
|
||||
"volume_ratio": volume / prior_volume if prior_volume > 0 else 0,
|
||||
"float_share": 0,
|
||||
"bid_size1": bid_size,
|
||||
"ask_size1": ask_size,
|
||||
"snapshot_time": str(row.get("time") or ""),
|
||||
"dynamic": True,
|
||||
}
|
||||
)
|
||||
return normalized
|
||||
Reference in New Issue
Block a user