4. 数据获取与清洗

跨品种与期限价差研究的第一道门槛,往往不是模型,而是数据。价差研究的本质是“同一逻辑链条上两个价格序列的相对关系”,因此对数据的时间一致性、合约一致性、口径一致性要求远高于单品种研究。本章围绕数据源选择、API接口调用、缺失值处理、异常值检测与数据对齐五个环节,给出可落地的工程化流程。

一、核心原理:价差研究对数据的三个硬约束

价差 = 价格A − 价格B(或比值)。这个减法看似简单,却隐含三个硬约束:

  • 时间戳对齐:两个序列若存在分钟级错位,价差会凭空产生“伪波动”。例如A在14:59:30更新,B在15:00:00更新,跨节假日前会出现虚假跳空。
  • 合约口径一致:跨期价差必须明确是“近月−远月”还是“主力−次主力”,且换月规则要统一,否则价差序列会在换月日出现非市场性断裂。
  • 复权与单位一致:跨品种价差常涉及不同报价单位(元/吨、美分/蒲式耳、点),需统一到同一量纲,否则统计量(均值、方差、相关系数)全部失真。
核心判断:价差数据的质量不取决于单个序列有多“干净”,而取决于两个序列在时间、合约、单位三个维度上是否严格可比。任何一处不一致,都会在价差层面被放大。

二、常用数据源与API接口

实战中数据源通常分三类:交易所官方、商业数据终端、开源/社区接口。选择依据是频率、历史深度、字段完整度、调用限制。

数据源类型 典型代表 优势 局限
交易所官方 上期所、大商所、郑商所、CFFEX 权威、免费、含结算价与持仓 接口分散、历史文件格式不统一
商业终端 Wind、同花顺iFinD、Choice 字段全、复权方便、支持批量 需授权、API有并发限制
开源/社区 AkShare、Tushare、yfinance 免费、Python友好、上手快 稳定性依赖社区、字段可能缺失

以AkShare获取期货主力连续合约为例,典型调用如下:

import akshare as ak
import pandas as pd

# 获取某品种主力连续日线
df = ak.futures_main_sina(symbol="RB0", start_date="20230101", end_date="20241231")
df = df.rename(columns={
    "日期": "date", "开盘价": "open", "最高价": "high",
    "最低价": "low", "收盘价": "close", "成交量": "volume"
})
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date").sort_index()
print(df.head())
接口使用建议:批量拉取时加time.sleep(0.5)避免触发限流;对返回结果先做dtypes检查,很多接口把价格返回为字符串,直接参与计算会报错或产生意外结果。

三、缺失值处理:先分类,再决策

缺失值不能一律删除或一律填充。按成因分为三类:

  1. 非交易时段缺失:夜盘、节假日、停牌。这类缺失是“真实无交易”,不应填充,应保留为NaN并在对齐时按交易日历裁剪。
  2. 单边缺失:A有数据、B无数据。常见于某合约流动性差。此时价差无法计算,应标记为无效而非插值。
  3. 随机缺失:接口丢包、字段为空。可用前值填充(ffill)或线性插值,但需限制连续填充长度。
# 限制连续填充长度,避免长段伪造数据
df["close"] = df["close"].ffill(limit=2)

# 标记单边缺失:两序列任一为空则价差无效
spread = (df_a["close"] - df_b["close"]).where(
    df_a["close"].notna() & df_b["close"].notna()
)
常见误区:对停牌或非交易时段做插值,会人为制造“零波动”区间,导致价差方差被低估、协整检验出现伪显著。缺失值处理的第一原则是区分“无交易”与“无数据”。

四、异常值检测与数据对齐

异常值检测在价差研究中比单序列更敏感,因为价差本身波动小,一个错误价格就能主导统计量。推荐组合使用三种方法:

  • 绝对阈值法:价差超过历史分位数(如99.5%)或涨跌停板幅度,标记为可疑。
  • 滚动Z-Score:z = (spread - rolling_mean) / rolling_std,|z|>4 触发告警。
  • 跳变检测:相邻两期变化率超过阈值,且下一期回归,判定为瞬时错误。
# 滚动Z-Score异常检测
roll_mean = spread.rolling(60, min_periods=30).mean()
roll_std  = spread.rolling(60, min_periods=30).std()
zscore = (spread - roll_mean) / roll_std
outliers = zscore.abs() > 4

# 跳变检测:单期变化超过5倍历史波动
jump = spread.diff().abs() > 5 * spread.diff().abs().rolling(60).median()

数据对齐是最后一步,也是最容易出错的一步。对齐的核心是统一时间轴:先取两序列时间戳的并集或交集,再按交易日历重采样。对于跨品种价差,建议使用交集对齐,只保留两品种都有交易的时点,避免单边缺失污染价差。

# 交集对齐:只保留两序列都有数据的时点
aligned = pd.concat([df_a["close"], df_b["close"]], axis=1, join="inner")
aligned.columns = ["close_a", "close_b"]
aligned["spread"] = aligned["close_a"] - aligned["close_b"]

# 按交易日历重采样到日频
aligned = aligned.resample("1D").last().dropna()
对齐检查清单:
  • 两序列时间戳是否同一时区、同一频率?
  • 是否使用同一交易日历(尤其跨市场、跨时区)?
  • 换月规则是否统一?主力合约切换日是否一致?
  • 价格单位是否统一?是否使用同一价格类型(收盘价/结算价)?
  • 对齐后价差序列是否存在异常跳空?

五、案例复盘:一次跨品种价差数据事故

某研究中使用螺纹钢与热卷主力连续合约构建价差。初版代码直接对两个主力连续序列做减法,结果价差在换月日出现约200点的跳空,协整检验p值从0.03变为0.21。复盘发现:螺纹钢主力在月初换月,热卷在月中换月,两个序列的换月日不同步,导致价差在换月窗口内混入了“新旧合约价差”而非真实品种价差。

修正方案:改用同一换月规则(如按持仓量最大合约),并在换月日对价差做拼接调整,即用新旧合约价差的变化量修正历史序列,而非直接拼接价格。修正后价差序列连续,协整检验恢复显著。

经验总结:跨品种与期限价差的数据清洗,重点不在“清洗”本身,而在对齐逻辑的设计。建议把数据获取、对齐、异常检测写成可复用的管道函数,每次研究前先跑一遍数据质量报告,再进入建模环节。

至此,数据获取与清洗的完整链路已经建立:从数据源选择与API调用,到缺失值分类处理、异常值组合检测,再到时间轴与合约口径的对齐。这条链路的稳健性,直接决定了后续价差统计研究结论的可信度。