1. 逐笔数据与特征工程概览
逐笔特征批量计算与流水线部署,解决的不是“某个指标怎么算”,而是“每天数亿条逐笔记录如何稳定、可复现地变成研究可用的特征表”。在动手写第一行计算代码之前,需要先把三件事看清楚:逐笔数据长什么样、特征计算在量化研究链条中处于什么位置、批量与流水线要跨过哪些坑。本节围绕这三点建立整体认知框架。
一、逐笔成交与逐笔委托的数据结构
沪深市场的逐笔数据通常分为两类:逐笔成交(Transaction)与逐笔委托(Order)。两者字段不同、语义不同,但通过证券代码、日期和撮合序号可以对齐。以常见的交易所 Level-2 逐笔数据为例,核心字段大致如下(字段名因数据商而异,语义一致):
| 数据类别 | 关键字段 | 语义说明 |
|---|---|---|
| 逐笔成交 | 证券代码、成交时间、成交序号、成交价格、成交数量、买卖方向 | 方向字段标识主动方:B 为主动买(外盘),S 为主动卖(内盘),部分数据商以 1/2 编码 |
| 逐笔委托 | 证券代码、委托时间、委托序号、委托价格、委托数量、买卖方向、订单类型 | 订单类型区分市价单、限价单、本方最优等;深市逐笔委托还包含撤单记录 |
理解结构时有几个容易踩的细节。第一,时间戳精度:逐笔数据通常精确到毫秒甚至微秒,同一毫秒内可能有多笔记录,排序必须依赖“时间 + 序号”复合键,不能只用时间。第二,序号连续性:成交序号与委托序号各自独立递增,序号跳变往往意味着数据缺失,这是后续质量校验的重要信号。第三,撤单的表达方式:部分市场用独立的撤单记录(数量为负或带撤单标志),部分市场用订单类型字段表达,特征计算时必须先统一口径,否则“委托总量”会被系统性高估或低估。第四,集合竞价时段的记录语义与连续竞价不同,批量计算时通常需要按交易阶段打标签或直接剔除。
从计算视角看,逐笔数据是典型的“事件流”:每条记录是一个带时间戳的事件,特征本质上是对事件流做窗口聚合或状态累积。这一认识直接决定了后续实现方式——按股票、按交易日分组后顺序扫描,比逐条独立计算高效得多。
二、特征计算在量化研究中的位置
一条完整的研究链条大致是:原始行情 → 数据清洗与对齐 → 特征计算 → 因子合成与检验 → 组合构建 → 回测与实盘。特征计算处在承上启下的位置:向上,它依赖清洗后的逐笔数据质量;向下,它输出的特征表直接决定因子研究的起点。这个位置带来两个约束:其一,特征必须可复现,同样的输入在任何时间、任何机器上跑出同样的结果;其二,特征必须可对齐,时间窗口、股票池、复权口径要与下游因子检验保持一致。
逐笔特征大致可分为几类:流量类(成交量、成交额、主动买卖量及占比)、订单类(委托笔数、委托量、撤单率、大单占比)、微观结构类(买卖压力失衡、订单簿斜率近似、成交价差)、时序类(滚动波动、量价相关性、自相关)。这些特征大多不是对单条记录的计算,而是对一段时间窗口内事件流的统计,因此“窗口如何切分”本身就是特征定义的一部分。
三、批量计算与流水线部署的整体目标与挑战
整体目标可以概括为一句话:把“按天、按股票、按窗口”的重复计算,变成一条可调度、可监控、可回溯的自动化流水线。典型形态是:每日收盘后触发,读取当日逐笔数据,按股票并行计算特征,写出分区特征表,并产出质量报告。挑战主要集中在四个方面。
- 数据规模与性能:单日全市场逐笔记录可达数亿条,单机逐条 Python 循环不可行,需要向量化或按股票分片并行。
- 口径一致性:方向编码、撤单处理、时间窗口边界(左闭右开还是左开右闭)必须在所有特征间统一,否则特征之间不可比。
- 可复现与版本化:特征定义、参数、代码版本要随输出一起记录,否则历史特征无法解释。
- 质量监控:缺失交易日、序号跳变、异常价格、特征分布漂移都需要自动检测并告警,而不是等因子失效后回头排查。
下图把上述关系组织成一条从原始逐笔事件到特征产出的数据流,并标出关键的质量控制点:
四、一个最小可运行的示例口径
为了让后续章节有统一的参照,这里先约定一套示例口径(属于教学假设,非实测结果)。假设研究标的为某只股票某交易日,逐笔成交按“时间 + 成交序号”升序排列,方向字段 B 记为 +1、S 记为 −1。定义 1 分钟窗口为左闭右开 [t, t+60s),则窗口内主动买入量占比可写为:
# 示例口径:按股票、按分钟窗口聚合
# df 列:code, ts, price, volume, side(+1/-1), seq
df = df.sort_values(["code", "ts", "seq"])
df["minute"] = df["ts"].dt.floor("1min")
df["signed_vol"] = df["volume"] * df["side"]
agg = df.groupby(["code", "minute"]).agg(
vol_sum=("volume", "sum"),
signed_vol_sum=("signed_vol", "sum"),
trade_cnt=("volume", "size"),
)
agg["active_buy_ratio"] = (agg["signed_vol_sum"] + agg["vol_sum"]) / (2 * agg["vol_sum"])
这段代码体现三个要点:先排序保证事件顺序正确;用 floor("1min") 实现左闭右开窗口;用带符号成交量一次性得到主动买卖净额,再换算为占比。实际工程中还需处理空窗口(无成交的分钟应补零还是跳过)、停牌日、集合竞价剔除等边界情况,这些将在后续章节展开。