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