2. 板块上涨家数的基础计算
在板块宽度分析中,最基础也最容易被低估的一步,是“把上涨家数算准”。很多分析结论之所以失真,并不是因为指标选错了,而是因为成分股口径没有统一:停牌股被当成平盘、ST股被混入正常样本、新股上市首日被计入涨跌统计,都会让上涨家数出现系统性偏差。本节围绕三个基础计数——上涨家数、下跌家数、平盘家数——说明定义、计算函数的设计,以及特殊成分股的处理方式。
三个基础计数的定义
设某一交易日板块内参与统计的成分股集合为 S,对每只股票 i,取其当日涨跌幅 pct_i(单位:%)。三个计数的定义如下:
- 上涨家数:满足
pct_i > 0的股票数量,记为up。 - 下跌家数:满足
pct_i < 0的股票数量,记为down。 - 平盘家数:满足
pct_i = 0的股票数量,记为flat。
这里有一个必须提前说明的参照约定:涨跌幅以前收盘价为基准,pct_i = (今收 - 前收) / 前收 × 100%。正号表示上涨,负号表示下跌,零表示收盘价与前收完全相等。全文与图示均采用这一约定,不做切换。需要强调的是,平盘不等于停牌:停牌股当日没有成交,其涨跌幅通常记为 0 或空值,如果直接按 pct_i = 0 归入平盘,就会把“无法交易”误当成“价格未动”,这是最常见的口径错误。
另一个容易忽略的细节是有效样本数。设 valid = up + down + flat,它应当等于“当日可正常交易且纳入统计的成分股数量”,而不是板块成分股总数。只有当 valid 与成分股总数一致时,上涨家数才具有跨日可比性;否则应同时记录有效样本数,供后续宽度指标做归一化。
计算函数的设计
把上述定义落成一个可复用的函数,关键在于输入数据的结构。假设我们已有一张按“交易日 × 股票”组织的行情表,字段包括 trade_date、code、pct_chg、is_suspend(是否停牌)、is_st(是否 ST)、list_days(上市天数)。函数的目标是:对指定板块、指定交易日,返回三个计数及有效样本数。
def calc_breadth(quotes, members, trade_date,
exclude_st=True, min_list_days=1):
"""
quotes: 行情表,含 code / trade_date / pct_chg / is_suspend / is_st / list_days
members: 板块成分股代码集合
trade_date: 统计日期
返回: dict(up, down, flat, valid, suspended, excluded)
"""
day = quotes[(quotes.trade_date == trade_date) &
(quotes.code.isin(members))]
suspended = day[day.is_suspend].code.nunique()
# 剔除停牌
tradable = day[~day.is_suspend]
# 剔除上市不足 min_list_days 的新股
tradable = tradable[tradable.list_days >= min_list_days]
# 可选剔除 ST
if exclude_st:
tradable = tradable[~tradable.is_st]
pct = tradable.pct_chg.dropna()
up = int((pct > 0).sum())
down = int((pct < 0).sum())
flat = int((pct == 0).sum())
return {
"up": up, "down": down, "flat": flat,
"valid": up + down + flat,
"suspended": int(suspended),
"excluded": len(members) - (up + down + flat) - int(suspended)
}
这段实现体现了三个处理顺序:先按板块成分股过滤,再剔除停牌,最后处理新股与 ST。顺序不能随意调换——如果先剔除 ST 再剔除停牌,suspended 的计数就会漏掉停牌中的 ST 股,导致“有效样本 + 停牌 + 剔除”无法与成分股总数对齐。函数返回的 excluded 是一个校验项:up + down + flat + suspended + excluded 应等于板块成分股总数,若不等,说明成分股名单与行情表存在未匹配的代码,需要先排查数据对齐问题。
特殊成分股的处理原则
停牌股:不参与涨跌统计,单独计数。停牌期间价格未更新,若计入平盘会人为压低波动、虚增平盘比例;若计入上涨或下跌则更无依据。处理方式是整体剔除,并在结果中保留 suspended 字段,便于观察板块内停牌比例是否异常升高。
ST 股:是否剔除取决于分析目的。ST 股涨跌幅限制通常为 ±5%,与主板 ±10%、创业板/科创板 ±20% 不同,混在一起会让“上涨家数”的含义变得模糊。若分析目标是衡量板块整体的做多情绪,建议剔除 ST 并单独统计;若目标是观察全样本的涨跌分布,则应保留但在结果中标注 ST 家数。函数中用 exclude_st 开关控制,避免把口径写死。
新股:上市初期(尤其首日及前若干日)没有涨跌幅限制或波动极大,且缺乏可比的前收基准,容易对上涨家数造成一次性冲击。常用做法是设置 min_list_days,例如剔除上市不足 1 个交易日或不足 5 个交易日的股票。参数取值应在整个分析周期内保持一致,否则跨日比较会失真。
up、down、flat、valid、suspended。前三个是基础计数,后两个用于校验与解释,缺一不可。计算流程与口径校验
下图把从成分股名单到三个计数的处理链路画出来,重点标出停牌、新股、ST 三个分流点,以及最终的对齐校验关系。
图中“对齐校验”是整条链路的收口。实际运行时,如果某日校验不通过,最常见的原因是成分股名单更新滞后于行情表,或某只股票代码发生变更导致匹配失败。此时不应直接使用当日的上涨家数,而应先补齐名单或修正代码映射,再重新计算。
一个可核对的计算示例
假设某板块共有 50 只成分股,某一交易日的情况为:停牌 2 只;上市不足 5 个交易日的新股 1 只;ST 股 3 只(按剔除口径处理);其余 44 只正常交易。在这 44 只中,上涨 26 只、下跌 15 只、平盘 3 只。则:
| 项目 | 数量 | 说明 |
|---|---|---|
| 板块成分股总数 | 50 | 名单口径 |
| 停牌 suspended | 2 | 剔除,不参与涨跌统计 |
| 新股 excluded | 1 | 上市不足 5 日,剔除 |
| ST 剔除 | 3 | 按 exclude_st=True 处理 |
| 有效样本 valid | 44 | 26 + 15 + 3 |
| 上涨 up | 26 | pct_chg > 0 |
| 下跌 down | 15 | pct_chg < 0 |
| 平盘 flat | 3 | pct_chg = 0 |
校验:26 + 15 + 3 + 2 + 1 + 3 = 50,与成分股总数一致,说明口径闭合。若不做剔除,直接把 50 只全部按涨跌幅符号计数,停牌股与新股会被错误地归入平盘或涨跌,上涨家数可能被高估或低估数只,在宽度指标的日度序列上形成噪声。
实现时的几个注意点
第一,pct_chg 的精度问题。行情数据常保留两位小数,理论上应为 0 的平盘可能因四舍五入显示为 0.00,也可能出现 -0.00。建议在比较前统一做数值规整,例如将绝对值小于 1e-6 的值置为 0,避免平盘家数被漏计。
第二,停牌判断应以行情表的停牌标记为准,而不是以“成交量为 0”推断。部分股票当日可能因极端行情出现零成交,但并未停牌,二者含义不同。
第三,ST 状态与新股天数都是时变属性,必须使用“当日”的状态,而不是当前最新状态。若用最新 ST 名单回溯历史,会把历史上尚未 ST 的股票错误剔除,造成前视偏差。
第四,函数应保持纯计算、不修改输入数据,便于在多个板块、多个日期上批量调用。批量计算时,把结果按 trade_date 排序后落表,字段固定为 up / down / flat / valid / suspended / excluded,后续的上涨家数占比、涨跌家数比等宽度指标都从这张表派生,口径自然统一。
valid,否则停牌与剔除较多的交易日会被系统性低估,跨日比较失去意义。