4. 因子计算所需衍生字段工程
在封板稳定性因子的历史检验中,原始行情数据往往只提供最基础的字段:涨停价、收盘价、成交量、成交额、流通股本、首次封板时间、最后封板时间、开板次数等。这些字段单独看信息量有限,直接拿来做因子既不稳定也不可比。真正决定因子质量的,是能否把它们加工成具有经济含义、量纲统一、可跨股票跨日期比较的衍生变量。本节要做的,就是把原始字段“翻译”成因子计算所需的中间变量,并组织成一张结构清晰的宽表。
为什么必须先做衍生字段工程
原始字段存在三个典型问题。第一是量纲不统一:封单量以“手”或“股”计,封单额以“元”计,不同市值、不同价位的股票之间无法直接比较,一只流通盘 20 亿的小盘股封单 5000 万股和一只流通盘 200 亿的大盘股封单 5000 万股,封板强度天差地别。第二是口径不一致:开板次数是绝对计数,5 次开板对一只高波动小盘股和一只低波动大盘股的意义不同。第三是时间信息分散:首次封板时间、最后封板时间、开板时段各自独立,无法直接反映“封板持续了多久、占全天交易时间的多大比例”。衍生字段工程的核心任务,就是用比值、归一化、占比这三类手段,把不可比的原始量转化为可比的强度量。
四个核心衍生变量的构造口径
封单量占流通盘比衡量的是“买盘封单相对可交易筹码的厚度”。计算口径为:封单量 ÷ 流通股本。封单量取收盘时刻买一价上的未成交委托量(若数据源提供逐笔委托,可取最后封板后的稳定封单量)。该比值越大,说明封板资金相对流通筹码越雄厚,封板越难被撬开。示例中某股流通股本 8 亿股,收盘封单 6400 万股,则封单量占流通盘比为 8%。经验上该值低于 1% 属于弱封,3%~8% 属于中等偏强,超过 10% 需警惕是否为小盘股极端值。
封单额占成交额比衡量的是“封板资金相对当日换手活跃度的强度”。计算口径为:封单额 ÷ 当日成交额,其中封单额 = 封单量 × 涨停价。这个比值反映的是资金在封板上的“决心”相对于全天博弈规模的大小。示例中封单额 6.4 亿元、当日成交额 12 亿元,比值为 53%,说明封单规模超过全天成交额的一半,封板意愿强烈。该指标与上一个指标互补:前者看筹码结构,后者看资金博弈。
开板次数归一化解决的是绝对计数不可比的问题。常用做法有两种:一是除以当日有效交易分钟数得到“每分钟开板频次”,二是用开板次数 ÷ (开板次数 + 1) 做饱和压缩,把 0~∞ 映射到 0~1 区间。实战中更推荐前者配合封板时长使用,因为频次具有明确的时间含义。示例中某股全天开板 3 次、有效交易 240 分钟,则开板频次为 0.0125 次/分钟。
封板时长占比衡量封板的持续性。计算口径为:(最后封板时间 − 首次封板时间 − 累计开板时长) ÷ 全天有效交易时长。若数据源只提供首次和最后封板时间而不提供开板时长,可用简化口径:(最后封板时间 − 首次封板时间) ÷ 全天交易时长,并在字段说明中标注口径差异。示例中首次封板 09:45、最后封板 14:20、全天交易 240 分钟,简化占比约 77%,说明大部分时间处于封板状态。
宽表的组织方式与字段规范
衍生变量算完后,需要落到一张“一行对应一只股票一个交易日”的宽表上。宽表的主键建议用 trade_date + ts_code,保证任意一行可唯一定位。字段组织上分三层:第一层是标识与口径字段,包括交易日期、股票代码、数据口径版本号,用于后续复现和排查;第二层是本节构造的四个核心衍生变量,以及首封时间、末封时间、是否尾盘封板等辅助字段;第三层是下游因子直接消费的标准化字段。宽表的价值在于“一次加工、多处复用”——后续无论做单因子分层回测、多因子合成还是分市场状态检验,都从这张表取数,避免每个环节重复计算导致口径漂移。
字段规范上有几个容易踩坑的细节必须提前约定。其一,时间字段统一转换为“距开盘的分钟数”再参与计算,避免跨午休时段直接相减造成高估;其二,封单量若数据源在收盘集合竞价阶段有变化,需明确取“连续竞价最后一笔”还是“收盘时点快照”,并在口径版本号中记录;其三,所有比值字段保留 4 位小数,归一化字段保留 6 位小数,防止后续排序时因精度不足产生并列;其四,对流通股本为 0 或成交额为 0 的异常行,统一置为空值而非 0,避免污染因子分布。
一段可执行的加工示例
下面用 Python 伪代码展示从原始表到宽表的关键加工步骤,示例参数均为教学假设值,实际使用时替换为真实数据源字段即可。
import pandas as pd
# raw: 原始行情表,一行一股票一日
raw["封单额"] = raw["封单量"] * raw["涨停价"]
raw["封单量占流通盘比"] = raw["封单量"] / raw["流通股本"]
raw["封单额占成交额比"] = raw["封单额"] / raw["成交额"]
# 时间转分钟数(以 09:30 为 0 点,午休不计入)
def to_min(t):
h, m = int(t[:2]), int(t[3:5])
mins = (h * 60 + m) - (9 * 60 + 30)
return mins - 90 if mins > 120 else mins # 扣除午休 90 分钟
raw["首封分钟"] = raw["首次封板时间"].map(to_min)
raw["末封分钟"] = raw["最后封板时间"].map(to_min)
raw["有效交易分钟"] = 240
raw["开板次数归一化"] = raw["开板次数"] / raw["有效交易分钟"]
raw["封板时长占比"] = (
raw["末封分钟"] - raw["首封分钟"] - raw["累计开板时长"]
) / raw["有效交易分钟"]
# 异常值处理:分母为 0 置空
for col, den in [("封单量占流通盘比", "流通股本"),
("封单额占成交额比", "成交额")]:
raw.loc[raw[den] <= 0, col] = None
wide = raw[[
"trade_date", "ts_code", "封单量占流通盘比", "封单额占成交额比",
"开板次数归一化", "封板时长占比", "首封分钟", "末封分钟",
"是否尾盘封板", "涨停价", "流通市值", "口径版本号"
]].copy()
加工完成后的自检要点
宽表生成后不要急着进入因子检验,先做三项自检。第一,分布检查:封单量占流通盘比应在 0~30% 区间内呈右偏分布,若出现大量超过 50% 的值,多半是流通股本字段取错(误用了总股本);封板时长占比应落在 0~100%,出现负值说明开板时长扣减逻辑有误。第二,缺失率检查:四个核心衍生变量的缺失率若超过 5%,需回溯原始字段的覆盖情况,尤其是首次封板时间在部分数据源中对“一字板”可能为空。第三,口径一致性检查:随机抽取 20 行手工复算,确认代码计算结果与手工计算一致,这一步能拦住绝大多数单位换算和午休扣减错误。
20240601_v2_午休扣减修正。当后续发现某字段口径需要调整时,新旧版本宽表可以并存,历史检验结果也能追溯到具体口径,避免“同一因子不同时间跑出不同结论”却查不到原因。至此,因子计算所需的中间变量已经齐备,宽表结构也已定型。后续的因子构建、分层回测与稳定性检验,都将以这张宽表为唯一数据入口,保证从字段到结论的链路清晰可复现。