4. 数据采集与清洗实战

执行成本测算的精度,很大程度上不取决于模型多复杂,而取决于底层数据是否干净、时间是否对齐、口径是否一致。同样是买入10万股,高流动性股票与低流动性股票在逐笔成交结构、盘口厚度、报价跳动频率上差异巨大,如果直接拿未经清洗的原始数据套用冲击成本模型,得到的滑点估计可能偏差数倍。本节围绕“采集—清洗—对齐—质检”四个环节,以高、中、低三档成交量的股票为对象,说明如何构建可直接用于成本测算的数据集。

一、明确采集对象与字段口径

成本测算通常需要两类数据:逐笔成交(Trade)与盘口快照(Quote/Snapshot)。逐笔成交记录每一笔撮合的成交价、成交量、成交方向(主动买/主动卖,通常由成交价与当时买卖一档价比较推断),是计算实现价差与市场冲击的基础;盘口快照按固定频率(A股常见为3秒一笔,部分数据源提供逐笔委托重建的更高频快照)记录买卖五档或十档的价格与挂单量,用于估计订单簿深度、买卖价差与瞬时流动性。

采集前必须先固定字段口径,否则后续对齐会反复返工。建议至少保留以下字段:

数据类别关键字段用途
逐笔成交证券代码、成交时间(精确到毫秒)、成交价、成交量、成交编号、买卖方向标志计算VWAP、实现价差、方向性成交占比
盘口快照证券代码、快照时间、买一至买五价与量、卖一至卖五价与量、最新价、累计成交量估计买卖价差、盘口深度、冲击成本基准
辅助信息交易日历、复权因子、涨跌停价、最小报价单位剔除异常价、统一价格口径

需要特别强调时间戳口径:交易所撮合时间、行情落地时间、本地接收时间三者不同。成本测算应统一使用交易所撮合时间(逐笔成交的成交时间、快照的行情时间),并在数据说明中注明时区与精度,避免混用导致毫秒级错位。

二、按成交量分层抽样

不同成交量股票的数据特征差异明显,建议按近20日日均成交额分层抽样,每层选3~5只,形成对照样本。例如(以下为教学示例参数,非实测结果):高流动性层选取日均成交额50亿元以上的大盘股,中流动性层选取5亿~15亿元的中盘股,低流动性层选取5000万元以下的小盘股。分层的目的不是追求样本代表性,而是让后续清洗规则能覆盖不同数据形态——高流动性股票逐笔成交密集、快照更新快,低流动性股票则可能出现长时间无成交、盘口挂单稀疏、买卖价差异常宽的情况。

逐笔成交与盘口快照:采集 → 清洗 → 对齐 → 质检 逐笔成交 Trade 成交时间(ms) / 价 / 量 成交编号 / 买卖方向 盘口快照 Quote 快照时间 / 买一至买五 卖一至卖五 / 最新价 辅助信息 交易日历 / 复权因子 / 涨跌停价 清洗规则 ① 去重:按成交编号/快照时间 ② 剔除非交易时段记录 ③ 价格越界:超出涨跌停剔除 ④ 量价异常:负量/零价剔除 ⑤ 盘口交叉:买一≥卖一标记 ⑥ 停牌日整段剔除 ⑦ 方向推断:比较成交价与 买卖一档中价 ⑧ 单位统一:股/手、元 ⑨ 缺失快照:前值填充并标记 时间对齐 基准:交易所撮合时间 粒度:统一到秒或3秒 方法:as-of 向前匹配 每笔成交匹配其之前 最近一笔快照 容差:快照滞后≤3秒 超容差标记为未匹配 输出:成交-盘口宽表 保留原始时间戳列 便于回溯核对 质检 覆盖率 重复率 价差分布 成交额 对账偏差 未匹配率 异常占比 分层对比 分层样本与清洗关注点(示例参数) 高流动性层 日均成交额 > 50亿元 关注:逐笔密集、快照高频、去重 中流动性层 日均成交额 5亿~15亿元 关注:价差波动、方向推断准确性 低流动性层 日均成交额 < 5000万元 关注:长时间无成交、盘口稀疏、宽价差

三、清洗:把“脏数据”挡在测算之前

原始行情数据的常见问题包括重复推送、非交易时段残留记录、价格越界、盘口买卖价交叉、停牌日空快照等。清洗不是简单删行,而是要有明确的判定规则和留痕。以下给出一段可参考的清洗逻辑(Python/pandas风格,示例代码):

import pandas as pd

# 假设 trades 与 quotes 已按证券代码、日期读取
# 1) 去重:同一成交编号只保留一条
trades = trades.drop_duplicates(subset=["code", "trade_id"])

# 2) 剔除非交易时段(以A股连续竞价 09:30:00-11:30:00, 13:00:00-15:00:00 为例)
def in_session(ts):
    t = ts.time()
    return (pd.Timestamp("09:30:00").time() <= t <= pd.Timestamp("11:30:00").time()) or \
           (pd.Timestamp("13:00:00").time() <= t <= pd.Timestamp("15:00:00").time())
trades = trades[trades["trade_time"].apply(in_session)]

# 3) 价格越界:超出当日涨跌停价的记录剔除
trades = trades[(trades["price"] >= trades["limit_down"]) & (trades["price"] <= trades["limit_up"])]

# 4) 量价异常
trades = trades[(trades["volume"] > 0) & (trades["price"] > 0)]

# 5) 盘口交叉标记(买一价 >= 卖一价),不直接删除,先打标
quotes["crossed"] = quotes["bid1"] >= quotes["ask1"]

# 6) 方向推断:成交价高于买卖一档中价记为主动买,低于记为主动卖,相等记为中性
mid = (quotes["bid1"] + quotes["ask1"]) / 2
trades = trades.merge(quotes[["code", "quote_time", "bid1", "ask1"]], how="left", on=["code"])
trades["side"] = "中性"
trades.loc[trades["price"] > mid, "side"] = "主动买"
trades.loc[trades["price"] < mid, "side"] = "主动卖"

这段代码的关键不在语法,而在几个判断口径:非交易时段的边界是否包含集合竞价、涨跌停价是否使用当日实际值而非固定比例、盘口交叉是删除还是打标。对低流动性股票,盘口交叉和宽价差出现频率更高,若一律删除会损失大量有效样本,建议保留并打标,在测算时按标记分组统计。

四、对齐:让每笔成交找到“当时”的盘口

逐笔成交与盘口快照的时间粒度不同,不能简单按行拼接。正确做法是以成交时间为基准,为每笔成交匹配其之前最近一笔快照(as-of 向前匹配),并设置容差。例如快照间隔为3秒,则容差设为3秒;若某笔成交之前最近快照滞后超过3秒(常见于低流动性股票午间或尾盘),标记为“未匹配”,不参与依赖盘口的指标计算,但保留在成交统计中。

对齐后应形成一张宽表,每行是一笔成交,列中包含成交价、成交量、方向,以及匹配到的买一至买五价量、卖一至卖五价量、买卖价差、盘口深度等。这样后续计算实现价差(成交价与到达时中间价之差)、冲击成本(成交价与决策基准价之差)时,可以直接按行取数,避免反复回表。

五、质检:用几个硬指标判断数据集能否交付

清洗对齐完成后,不能凭感觉认为“差不多了”,而要用可量化的指标验收。建议至少检查以下几项,并分层对比:

  • 覆盖率:有效交易日中,逐笔成交与快照均非空的交易日占比,低流动性股票应重点关注停牌与无成交日。
  • 重复率:去重后记录数占原始记录数比例,异常偏高说明数据源推送存在问题。
  • 未匹配率:成交未能匹配到容差内快照的比例,高流动性股票应接近0,低流动性股票可接受但需说明。
  • 对账偏差:逐笔成交累计成交量与快照中累计成交量字段的偏差,用于发现漏笔或重复。
  • 价差分布:分层统计买卖价差均值与分位数,若低流动性层价差异常窄,需排查是否误用了非交易时段快照。
交付数据集时,建议同时附一份数据说明:时间戳口径、清洗规则版本、未匹配记录处理方式、分层样本清单。成本测算的结论要能追溯到数据口径,否则不同批次数据算出的滑点无法比较。

完成上述步骤后,得到的数据集应满足:每笔成交都有明确的方向标记和可追溯的盘口上下文,异常记录有标记而非被静默删除,分层样本的清洗结果可横向对比。这样的数据集才具备进入成本测算环节的资格——后续无论是计算VWAP偏离、估计冲击成本系数,还是比较多档成交量股票的執行成本差异,底层输入都是同一口径、同一时间基准的高质量数据。