4. 交易成本数据采集
佣金与滑点成本建模的质量,首先取决于输入数据的质量。很多回测结果失真,并不是模型公式写错了,而是数据在采集阶段就混入了错误费率、缺失价差或异常成交量。本节围绕“如何拿到数据、如何清洗、如何整理成可直接喂给模型的数据集”展开,重点讲清楚每一类数据的来源、字段含义、常见脏数据形态以及处理方式。
一、建模需要哪几类数据
交易成本建模通常需要三类基础数据,它们的时间粒度和更新频率并不相同,采集时必须分开处理:
- 佣金费率数据:包括券商佣金率、最低佣金、印花税、过户费、交易所规费等。这类数据变化频率低,但存在“按市场、按账户、按时间段”的差异,必须带生效日期。
- 买卖价差数据:最优买价(bid)、最优卖价(ask)、盘口深度。用于估计即时成交的价差成本,通常来自逐笔或快照行情。
- 成交量与成交价数据:用于估计冲击成本与滑点,需要成交价、成交量、成交笔数,以及对应的委托量信息。
三类数据的对齐粒度决定了后续建模口径。若用日频成交量估计冲击成本,却用分钟级价差估计价差成本,两者混用会导致成本量纲不一致。实践中建议先确定建模的最小时间粒度(例如分钟或日),再按该粒度统一采集。
二、佣金费率的采集与结构化
佣金费率不是单一数字,而是一组分段规则。以A股为例,一笔卖出交易的成本通常包含:券商佣金(含最低5元限制)、印花税(卖出单边)、过户费(按成交金额比例)。采集时应把每一项拆成独立字段,而不是只记录一个“综合费率”。
建议的费率表结构如下(示例参数为合理假设,非实测):
| 字段 | 含义 | 示例值 |
|---|---|---|
| market | 市场标识 | SH / SZ |
| side | 买卖方向 | buy / sell |
| commission_rate | 券商佣金率 | 0.00025 |
| min_commission | 最低佣金(元) | 5.00 |
| stamp_tax_rate | 印花税率 | 0.0005(仅卖出) |
| transfer_fee_rate | 过户费率 | 0.00001 |
| effective_from | 生效起始日 | 2023-08-28 |
| effective_to | 生效截止日 | 2024-12-31 |
关键点是带生效区间。费率会随政策调整(例如印花税下调),若只存一份“当前费率”并回填到全部历史,回测会系统性高估或低估成本。清洗时应检查区间是否连续、有无重叠,重叠区间必须人工确认优先级。
三、价差与成交数据的采集要点
价差数据的核心字段是时间戳、bid、ask、bid_size、ask_size。采集时最常见的三个问题:
- 时间戳时区不统一:不同数据源可能用UTC、交易所本地时间或带毫秒偏移,合并前必须统一到同一时区并明确标注。
- 盘口交叉或倒挂:出现 bid > ask 的记录,通常是数据源撮合快照不同步所致,属于脏数据,应剔除或标记。
- 零值与空值:停牌、集合竞价前后常出现 bid=0 或 ask 缺失,直接参与计算会得到无穷大或负价差。
成交量数据则要关注“成交量单位”和“复权口径”。同一只股票,不同源可能用股、手(100股)或千股为单位;价格是否复权也直接影响成交金额计算。采集阶段就应在元数据中写清单位与复权方式,避免下游误用。
四、数据清洗与预处理流程
下图展示了一条从原始数据到可建模数据集的清洗链路,重点标出了每一步的输入、处理动作与输出判断:
清洗过程中有两个原则值得强调。第一,能标记就不要直接删。异常记录被删除后,下游无法判断某段时间是“无数据”还是“数据被丢弃”,会给缺失值处理带来歧义。建议增加一个 quality_flag 字段,取值如 ok、crossed、zero_bid、stale,建模时按需过滤。第二,缺失值处理要区分场景:停牌期间没有盘口是正常现象,可用前值填充并标记;而连续多日无成交则可能意味着流动性枯竭,此时填充会掩盖真实成本,应标记为不可用样本。
五、一个可执行的预处理片段
下面用伪代码展示价差字段的清洗与特征计算逻辑,参数为示例假设:
# 输入: df 含 timestamp, bid, ask, bid_size, ask_size
# 1) 剔除盘口倒挂与零值
df["quality_flag"] = "ok"
df.loc[(df.bid <= 0) | (df.ask <= 0), "quality_flag"] = "zero_quote"
df.loc[df.bid > df.ask, "quality_flag"] = "crossed"
# 2) 计算中间价与相对价差(仅对有效记录)
valid = df[df.quality_flag == "ok"].copy()
valid["mid"] = (valid.bid + valid.ask) / 2
valid["rel_spread"] = (valid.ask - valid.bid) / valid.mid
# 3) 按分钟聚合,缺失分钟标记
minute = valid.set_index("timestamp").resample("1min").agg(
{"rel_spread": "mean", "mid": "last"}
)
minute["is_missing"] = minute.rel_spread.isna()
# 4) 输出质量摘要
print("倒挂占比:", (df.quality_flag == "crossed").mean())
print("零值占比:", (df.quality_flag == "zero_quote").mean())
print("分钟缺失率:", minute.is_missing.mean())
运行后应重点看三个比例:倒挂占比、零值占比、分钟缺失率。若倒挂占比超过千分之一,通常说明数据源撮合快照存在系统性问题,需要换源或调整对齐逻辑;若缺失率在某只股票上异常高,应检查是否长期停牌或数据覆盖不全。这些判断依据比单纯“跑通代码”更重要。
六、交付一份可建模数据集
最终交付物不应只是一张宽表,而应包含数据表与质量报告两部分。数据表按“标的—时间—方向”组织,至少包含:时间戳、标的代码、bid、ask、mid、rel_spread、成交量、成交额、佣金率、最低佣金、印花税率、quality_flag。质量报告则记录各字段缺失率、异常占比、费率区间覆盖情况以及已知问题清单。
cost_dataset_v3_rule2024Q1。当费率政策或清洗阈值调整时,版本号随之变化,回测结果才可追溯、可复现。做到这一步,数据才真正具备“可建模”的资格:字段含义明确、口径统一、异常可识别、缺失有交代。后续无论是估计价差成本还是拟合冲击成本函数,都能在此基础上稳定推进,而不会因为底层数据反复返工。