4. 数据采集与预处理实战
连续小额订单冲击行为研究的第一道门槛不在模型,而在数据。逐笔委托(order-by-order)与逐笔成交(trade-by-trade)是两套独立记录:委托流记录“谁在什么时刻挂出/撤掉了多少量、价格是多少”,成交流记录“什么时刻发生了撮合、成交价与成交量是多少”。研究连续小额订单,需要把这两套流按时间与订单标识对齐,再从中筛出“单笔金额小、时间间隔短、方向一致”的订单簇。本节围绕一条可复现的处理链展开:字段理解 → 清洗 → 对齐 → 序列重构 → 缺失与异常处理 → 质量校验。
一、先明确字段与参照约定
以常见的逐笔数据为例,委托表至少包含:order_id(委托编号)、time(委托到达时间,精确到毫秒或微秒)、price、volume、side(B/S)、order_type(限价/市价/本方最优等)、action(A=新增,D=删除/撤单)。成交表包含:trade_id、time、price、volume、bid_order_id、ask_order_id(买卖双方委托编号)、side(主动方方向)。
本节统一约定:时间轴以交易所撮合时钟为准,方向以“主动方”定义——买方主动成交记为 B,卖方主动成交记为 S;金额一律用“价格×数量”计算,数量单位与价格单位在预处理阶段就固定下来,避免后续混用。所有图示与正文均遵循这一约定。
二、清洗:把“脏”挡在序列之外
原始逐笔数据常见四类问题:重复记录(同一 order_id 多次出现)、时间戳乱序或精度不一致(部分为秒级、部分为毫秒级)、价格或数量为 0/负值、以及撤单记录缺少对应新增记录。处理顺序建议为:先去重(按 order_id + time + action 组合键),再统一时间精度(全部转为微秒整数),再剔除 price<=0 或 volume<=0 的记录,最后用“新增-撤单配对”检查孤儿撤单。
import pandas as pd
def clean_orders(df):
df = df.drop_duplicates(subset=["order_id", "time", "action"])
df["ts"] = (df["time"] * 1_000_000).astype("int64") # 统一为微秒
df = df[(df["price"] > 0) & (df["volume"] > 0)]
df = df.sort_values("ts").reset_index(drop=True)
# 孤儿撤单:撤单存在但无对应新增
added = set(df.loc[df["action"] == "A", "order_id"])
orphan = df[(df["action"] == "D") & (~df["order_id"].isin(added))]
return df, orphan
注意:去重不能简单按整行 drop_duplicates(),因为同一订单在不同毫秒可能有多条合法记录;必须带上 action 与时间戳。孤儿撤单不一定要删除,但应单独落表,作为数据质量指标记录。
三、对齐:委托与成交的“双向挂接”
对齐的核心是把每笔成交挂到买卖双方的委托上。做法是以成交表的 bid_order_id 与 ask_order_id 为外键,分别左连接委托表两次,得到买方委托的挂单时间、价格与卖方委托的挂单时间、价格。由此可计算两个关键量:被动方等待时长(成交时间 − 被动方挂单时间)与主动方到达即成交的延迟(成交时间 − 主动方挂单时间,通常接近 0)。
对齐时最容易出错的是“同一订单分多笔成交”。此时一个 order_id 会对应多条成交记录,连接后行数会膨胀,这是正常的,但必须在聚合时按 trade_id 去重,避免成交量被重复累计。另一个坑是时间戳相同但撮合顺序不同,需保留原始行号作为稳定排序键。
四、重构连续小额订单序列
“连续小额订单”没有唯一阈值,需要按标的流动性与研究目的设定。一个可操作的示例参数:单笔成交金额 ≤ 5 万元、相邻两笔同向成交时间间隔 ≤ 500 毫秒、连续笔数 ≥ 5。重构步骤为:先按主动方方向分组,再按时间排序,计算相邻成交的时间差 dt,用 dt 是否超阈值切分“簇”,最后过滤掉笔数不足或金额超限的簇。
def build_bursts(trades, amt_max=50000, dt_max=500_000, min_n=5):
t = trades.sort_values(["side", "ts"]).copy()
t["amount"] = t["price"] * t["volume"]
t["dt"] = t.groupby("side")["ts"].diff().fillna(10**12)
t["new_burst"] = (t["dt"] > dt_max) | (t["amount"] > amt_max)
t["burst_id"] = t.groupby("side")["new_burst"].cumsum()
g = t.groupby(["side", "burst_id"])
bursts = g.agg(n=("trade_id", "size"), amt=("amount", "sum"),
t0=("ts", "min"), t1=("ts", "max")).reset_index()
return bursts[bursts["n"] >= min_n]
这段逻辑的关键在于 new_burst 同时承担“时间断裂”和“金额超限”两种切分条件——任一条件触发即开新簇。这样得到的每个 burst_id 就是一条候选连续小额订单序列,后续可计算其持续时长、总金额、平均单笔金额与方向一致性。
五、缺失值与异常值:分类处理而非一刀切
缺失值分两类。第一类是结构性缺失,例如某笔成交的 bid_order_id 在委托表中查不到——这通常意味着委托数据被截断或跨日边界问题,应标记为“对齐失败”并统计比例,比例低于 0.1% 时可剔除该成交,高于 1% 则要回查数据源。第二类是字段缺失,如 order_type 为空,可用同标的同方向同时段众数填充,但必须新增一列 is_imputed 标记,避免污染后续统计。
异常值方面,价格类异常用“同标的当日成交价中位数 ± 10%”作粗筛,超出者多为数据错位;数量类异常用分位数法,例如超过 99.9 分位且金额超过 100 万元的单笔成交,先标记为“大单”而非直接删除——因为大单恰恰是研究小额订单冲击时的重要对照样本。时间类异常(dt 为负)说明排序键不稳定,应回到原始行号重新排序,而不是取绝对值。
六、结果如何看:一份可执行的校验清单
处理完成后,至少核对以下五项,任一项异常都应回到对应环节排查:
- 行数守恒:清洗后成交笔数 + 剔除笔数 = 原始笔数,差额可解释。
- 对齐失败率:成交挂不到委托的比例,正常应低于 0.1%。
- 时间单调性:按
ts排序后dt不应为负。 - 簇内方向一致率:同一
burst_id内主动方方向应完全一致,否则说明分组键有误。 - 金额分布:簇内平均单笔金额应显著低于全样本均值,这是“小额”筛选生效的直接证据。
以某示例标的的假设数据为例:原始成交 12 万笔,去重剔除 320 笔,对齐失败 47 笔(0.04%),重构得到 186 个连续小额订单簇,簇内平均单笔金额约 1.8 万元,而全样本平均单笔金额约 6.5 万元。这组数字仅用于说明校验口径,实际数值需以真实数据跑出。至此,一条可追溯、可复核的连续小额订单序列才算真正准备好,可以进入后续的冲击行为度量环节。