# -*- coding: utf-8 -*- """情绪温度计——涨停池三件套四指标(消费 provider.get_limit_pool 出口)。 原料:每日 19:30 ak-events 落盘的 zt/zbgc/dtgc 三池(get_limit_pool kind 三合一,英文标准列 code/consecutive_boards/industry…,真空日=标准列空表)。 四指标口径(与数据 session 契约对齐 2026-09-03): - 涨停/跌停数: 行数计;**dtgc 空=当日 0 跌停,是合法值非缺数** - 连板高度: zt.consecutive_boards 取 max(字符串值 to_numeric 吸收) - 炸板率: zbgc行数/(zt行数+zbgc行数)——涨停池含「炸过又回封」的票 (break_count>0 仍在 zt),两池并集=当日触板全集,勿用 zt 内 break_count>0 把回封票从分母里扣掉 - 涨停行业集中度: zt.industry top1 占比+行业名(空串/NaN 剔除) 定位:巡检/守卫层(veto/overlay)原料,不进信号层;阈值由 history_thermometer 对已积累历史校准后另行确定,本模块只算不判。 """ from typing import Any, Dict, Optional import pandas as pd def compute_thermometer( zt: pd.DataFrame, zbgc: pd.DataFrame, dtgc: pd.DataFrame, ) -> Dict[str, Any]: """单日四指标(纯函数,输入 get_limit_pool 三 kind 的当日切版)。""" zt_n, zbgc_n, dt_n = len(zt), len(zbgc), len(dtgc) touch_total = zt_n + zbgc_n max_boards = 0 if zt_n and "consecutive_boards" in zt.columns: boards = pd.to_numeric(zt["consecutive_boards"], errors="coerce") if boards.notna().any(): max_boards = int(boards.max()) if zt_n and "industry" in zt.columns: ind = zt["industry"].dropna().astype(str).str.strip() ind = ind[ind != ""] else: ind = pd.Series(dtype=object) if len(ind): counts = ind.value_counts() top_industry: Optional[str] = str(counts.index[0]) top1_ratio: float = float(counts.iloc[0]) / float(len(ind)) else: top_industry, top1_ratio = None, 0.0 return { "limit_up_count": zt_n, "limit_down_count": dt_n, "net_limit": zt_n - dt_n, "limit_up_down_ratio": (float(zt_n) / float(dt_n)) if dt_n else None, "max_consecutive_boards": max_boards, "broken_board_rate": (float(zbgc_n) / float(touch_total)) if touch_total else 0.0, "top_industry": top_industry, "industry_top1_ratio": round(top1_ratio, 4), } def daily_thermometer( provider: Any, date: Optional[str] = None, ) -> Dict[str, Any]: """当日快照:三 kind 单日拉取(单日即 raw 语义,节假日副本不入场)。 date=None → provider 的最新落盘日语义,trade_date 从返回尾行解析。 """ zt = provider.get_limit_pool("zt", date=date) zbgc = provider.get_limit_pool("zbgc", date=date) dtgc = provider.get_limit_pool("dtgc", date=date) result = compute_thermometer(zt, zbgc, dtgc) if date is not None: result["trade_date"] = str(date) elif "trade_date" in zt.columns and len(zt): result["trade_date"] = str(zt["trade_date"].iloc[-1]) else: result["trade_date"] = None return result def history_thermometer( provider: Any, start: str, end: Optional[str] = None, ) -> pd.DataFrame: """区间逐日指标(阈值校准用):三 kind 各一次区间拉取,按 trade_date 对齐。 日期并集驱动——某池某日无行(如跌停 0)按 0 计,不丢该日; trading_days_only 滤非交易日由 provider 承担,本层不再判历。 """ frames: Dict[str, pd.DataFrame] = {} for kind in ("zt", "zbgc", "dtgc"): df = provider.get_limit_pool(kind, start=start, end=end) frames[kind] = df if isinstance(df, pd.DataFrame) else pd.DataFrame() dates: set = set() for df in frames.values(): if not df.empty and "trade_date" in df.columns: dates.update(str(d) for d in df["trade_date"]) rows = [] for d in sorted(dates): slices = { kind: (df[df["trade_date"] == d] if not df.empty else df) for kind, df in frames.items() } row = compute_thermometer( slices["zt"], slices["zbgc"], slices["dtgc"]) row["trade_date"] = d rows.append(row) return pd.DataFrame(rows)