from __future__ import annotations from statistics import mean from typing import Any from backend.data.contracts import ProviderResult def build_snapshot( trade_date: str, previous_trade_date: str, inputs: dict[str, ProviderResult | dict[str, Any]], ) -> dict[str, Any]: daily_rows = _rows(inputs, "daily") daily = {str(row.get("ts_code") or ""): row for row in daily_rows} limits = _pool(_rows(inputs, "limit_up"), "涨停") broken = _pool(_rows(inputs, "broken"), "炸板") down_limits = _pool(_rows(inputs, "limit_down"), "跌停") previous_limits = _pool(_rows(inputs, "previous_limit_up"), "涨停") price_limits = {str(row.get("ts_code") or ""): row for row in _rows(inputs, "price_limits")} for row in broken: up_limit = _number(price_limits.get(row["identifier"], {}).get("up_limit")) row["distance_to_limit"] = ( round((up_limit - row["price"]) / up_limit * 100, 2) if up_limit else None ) yesterday = _yesterday( previous_limits, daily, limits, broken, down_limits, ) up_count = sum(_number(row.get("pct_chg")) > 0 for row in daily_rows) down_count = sum(_number(row.get("pct_chg")) < 0 for row in daily_rows) flat_count = len(daily_rows) - up_count - down_count amount = sum(_amount_yuan(row) for row in daily_rows) seal_rate = len(limits) / max(len(limits) + len(broken), 1) * 100 sectors = _sectors(limits) previous_sectors = _sectors(previous_limits) overview = { "up_count": up_count, "down_count": down_count, "flat_count": flat_count, "limit_up": len(limits), "limit_down": len(down_limits), "broken": len(broken), "seal_rate": round(seal_rate, 1), "amount": round(amount, 2), } return { "trade_date": trade_date, "previous_trade_date": previous_trade_date, "overview": overview, "limits": limits, "broken": broken, "down_limits": down_limits, "yesterday_limits": yesterday, "limit_performance": _performance(yesterday), "ladders": _ladders(limits), "sectors": sectors, "sector_rotation": _rotation(sectors, previous_sectors), } def build_realtime_inputs( inputs: dict[str, ProviderResult | dict[str, Any]], directory: dict[str, dict[str, Any]], ) -> dict[str, ProviderResult | dict[str, Any]]: daily_result = inputs.get("daily") limits_result = inputs.get("price_limits") previous_result = inputs.get("previous_limit_up") if not isinstance(daily_result, ProviderResult) or not isinstance( limits_result, ProviderResult ): return inputs prices = {str(row.get("ts_code") or ""): row for row in limits_result.rows} previous = { str(row.get("ts_code") or ""): row for row in previous_result.rows } if isinstance(previous_result, ProviderResult) else {} pools: dict[str, list[dict[str, Any]]] = { "limit_up": [], "limit_down": [], "broken": [], } for quote in daily_result.rows: identifier = str(quote.get("ts_code") or "") price = prices.get(identifier) or {} current = _number(quote.get("close")) high = _number(quote.get("high")) up_limit = _number(price.get("up_limit")) down_limit = _number(price.get("down_limit")) event_type = "" if up_limit > 0 and current >= up_limit - 0.001: event_type = "limit_up" elif up_limit > 0 and high >= up_limit - 0.001: event_type = "broken" elif down_limit > 0 and current <= down_limit + 0.001: event_type = "limit_down" if not event_type: continue identity = directory.get(identifier) or {} prior_streak = int(_number(previous.get(identifier, {}).get("limit_times"))) pools[event_type].append( { **quote, "name": str(quote.get("name") or identity.get("name") or ""), "industry": str(identity.get("sector") or ""), "first_time": "", "last_time": "", "open_times": 0, "limit_times": prior_streak + 1 if event_type == "limit_up" else 1, "fd_amount": 0, } ) metadata = daily_result.metadata return { **inputs, "limit_up": ProviderResult(tuple(pools["limit_up"]), metadata), "limit_down": ProviderResult(tuple(pools["limit_down"]), metadata), "broken": ProviderResult(tuple(pools["broken"]), metadata), } def _rows( inputs: dict[str, ProviderResult | dict[str, Any]], key: str ) -> tuple[dict[str, Any], ...]: value = inputs.get(key) return value.rows if isinstance(value, ProviderResult) else () def _pool(rows: tuple[dict[str, Any], ...], status: str) -> list[dict[str, Any]]: result = [] for row in rows: identifier = str(row.get("ts_code") or "") amount = _amount_yuan(row) result.append( { "identifier": identifier, "code": identifier.split(".")[0], "name": str(row.get("name") or "").strip(), "price": _number(row.get("close")), "change": _number(row.get("pct_chg")), "sector": str(row.get("industry") or "").strip(), "reason": "", "first_time": _time(row.get("first_time")), "last_time": _time(row.get("last_time")), "open_times": int(_number(row.get("open_times"))), "streak": max(1, int(_number(row.get("limit_times"), 1))), "turnover_rate": _number(row.get("turnover_ratio")), "amount": amount, "seal_amount": _number(row.get("fd_amount")), "float_market_value": _number(row.get("float_mv")), "status": status, } ) return result def _yesterday( previous: list[dict[str, Any]], daily: dict[str, dict[str, Any]], current: list[dict[str, Any]], broken: list[dict[str, Any]], down: list[dict[str, Any]], ) -> list[dict[str, Any]]: current_map = {row["identifier"]: row for row in current} broken_codes = {row["identifier"] for row in broken} down_codes = {row["identifier"] for row in down} result = [] for prior in previous: identifier = prior["identifier"] quote = daily.get(identifier, {}) change = _number(quote.get("pct_chg")) if identifier in current_map: outcome = "晋级" elif identifier in broken_codes: outcome = "炸板" elif identifier in down_codes: outcome = "跌停" elif change > 0: outcome = "红盘" else: outcome = "断板" result.append( { "identifier": identifier, "code": prior["code"], "name": prior["name"], "prior_streak": prior["streak"], "current_streak": current_map.get(identifier, {}).get("streak", 0), "current_change": change, "current_price": _number(quote.get("close")), "sector": prior["sector"], "reason": prior["reason"], "outcome": outcome, } ) return result def _performance(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: result = [] for level in sorted({int(row["prior_streak"]) for row in rows}, reverse=True): group = [row for row in rows if int(row["prior_streak"]) == level] advanced = sum(row["outcome"] == "晋级" for row in group) positive = sum(_number(row["current_change"]) > 0 for row in group) outcomes = { outcome: sum(row["outcome"] == outcome for row in group) for outcome in ("晋级", "红盘", "断板", "炸板", "跌停") } result.append( { "level": level, "count": len(group), "advanced": advanced, "red": outcomes["红盘"], "broken": outcomes["断板"], "opened": outcomes["炸板"], "limit_down": outcomes["跌停"], "advance_rate": round(advanced / len(group) * 100, 1), "positive_rate": round(positive / len(group) * 100, 1), "average_change": round(mean(_number(row["current_change"]) for row in group), 2), } ) return result def _ladders(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: levels = sorted({int(row["streak"]) for row in rows}, reverse=True) return [ { "level": level, "label": "首板" if level == 1 else f"{level}板", "count": sum(int(row["streak"]) == level for row in rows), "stocks": sorted( (row for row in rows if int(row["streak"]) == level), key=lambda row: row.get("first_time") or "99:99", ), } for level in levels ] def _sectors(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: names = sorted({str(row.get("sector") or "").strip() for row in rows} - {""}) result = [] for name in names: stocks = [row for row in rows if str(row.get("sector") or "").strip() == name] leader = max(stocks, key=lambda row: (int(row["streak"]), _number(row["amount"]))) max_streak = max(int(row["streak"]) for row in stocks) count = len(stocks) result.append( { "name": name, "count": count, "strength": min(100, 44 + count * 8 + max_streak * 5), "amount": round(sum(_number(row["amount"]) for row in stocks), 2), "leader": leader["name"], "representative": leader["identifier"], "change": round(mean(_number(row["change"]) for row in stocks), 2), "max_streak": max_streak, } ) return sorted( result, key=lambda row: (int(row["count"]), int(row["max_streak"]), _number(row["amount"])), reverse=True, )[:20] def _rotation( current: list[dict[str, Any]], previous: list[dict[str, Any]] ) -> list[dict[str, Any]]: previous_map = {str(row["name"]): row for row in previous} result = [] for rank, sector in enumerate(current[:12], start=1): previous_count = int(previous_map.get(str(sector["name"]), {}).get("count") or 0) delta = int(sector["count"]) - previous_count result.append( { **sector, "rank": rank, "previous_count": previous_count, "delta": delta, "trend": "升温" if delta > 0 else "降温" if delta < 0 else "持平", } ) return result def _number(value: Any, default: float = 0.0) -> float: try: number = float(value) return number if number == number else default except (TypeError, ValueError): return default def _amount_yuan(row: dict[str, Any]) -> float: amount = _number(row.get("amount")) return amount if row.get("amount_unit") == "yuan" else amount * 1000 def _time(value: Any) -> str: text = str(value or "").strip().replace(":", "") if len(text) < 4 or not text[:4].isdigit(): return "" return f"{text[:2]}:{text[2:4]}"