Files
sanguo_vnpy_v2/sanguo_portfolio/sentiment_thermometer.py
T

114 lines
4.2 KiB
Python

# -*- 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)