4. 数据获取与清洗

量价信号建模的第一道生死线不在模型,而在数据。行情数据看似只是“日期+OHLCV”,但真实工程中,数据源差异、接口限流、停牌缺失、涨跌停异常、复权口径不一致、多标的对齐错位,都会让后续因子与标签彻底失真。本节围绕“取得到、洗得净、对得齐”三条主线,建立可复用的数据管道。

一、常见数据源与API接口

按获取方式,行情数据源可分为三类:本地文件型(CSV、Parquet、HDF5,适合历史回测)、在线API型(交易所、数据商、开源库,适合增量更新)、数据库型(MySQL、ClickHouse、DuckDB,适合多标的批量查询)。选择时重点看四个维度:复权方式是否可指定、字段是否含成交额与换手率、历史深度、限流与稳定性。

以开源库为例,常见调用方式如下:

import akshare as ak
import pandas as pd

# 日线行情(前复权)
df = ak.stock_zh_a_hist(
    symbol="600519",
    period="daily",
    start_date="20200101",
    end_date="20241231",
    adjust="qfq"          # qfq前复权 / hfq后复权 / ""不复权
)
df = df.rename(columns={
    "日期":"date","开盘":"open","收盘":"close",
    "最高":"high","最低":"low","成交量":"volume","成交额":"amount"
})
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date").sort_index()
print(df.head())
接口调用务必做三件事:重试(网络抖动)、限流(sleep或令牌桶)、落盘(原始数据先存Parquet再加工,避免重复请求)。

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

行情缺失不是随机噪声,而是有明确成因。常见有四类:停牌(无成交,OHLC可能为空或等于前收)、非交易日(周末、节假日)、新股未上市(上市前无数据)、接口丢包(偶发空值)。处理策略必须与成因匹配,否则会制造虚假信号。

  • 停牌:不要简单前向填充价格后当作可交易。正确做法是保留价格填充用于计算,但增加 is_tradable 标记,回测时禁止在停牌日成交。
  • 非交易日:用交易日历对齐,而不是自然日。缺失的周末不应被填充为交易日。
  • 新股:上市前直接剔除,不做填充,避免引入未来信息。
  • 接口丢包:小比例可前向填充并记录日志;大比例应重新拉取。
# 交易日历对齐 + 停牌标记
cal = pd.DatetimeIndex(ak.tool_trade_date_hist_sina()["trade_date"])
df = df.reindex(cal)                      # 只保留交易日
df["is_tradable"] = df["volume"].fillna(0) > 0
df[["open","high","low","close"]] = df[["open","high","low","close"]].ffill()
df["volume"] = df["volume"].fillna(0)

三、异常值检测:量价数据的“体检”

异常值检测要区分真实极端行情与数据错误。涨停、跌停、巨量成交是真实信号,不能一刀切删除;而价格为负、最高价低于最低价、单日涨幅超过涨跌停限制、成交量突增百倍,则多为错误。

检测项判定规则处理建议
价格逻辑high < low 或 close 超出 [low, high]标记并回查原始源
涨跌幅|日收益| > 涨跌停限制(如11%)核对除权除息日
成交量volume <= 0 但价格变动视为停牌或错误
统计离群收益率 z-score > 6人工复核,不直接删
重复记录同一日期出现多行去重保留最新
# 逻辑校验与统计离群
bad = df[(df["high"] < df["low"]) |
         (df["close"] > df["high"]) |
         (df["close"] < df["low"])]
ret = df["close"].pct_change()
z = (ret - ret.mean()) / ret.std()
outlier = df[z.abs() > 6]
print("逻辑错误:", len(bad), "统计离群:", len(outlier))
不要用 dropna() 或 clip() 无差别清洗。删除涨停日会系统性削弱动量因子,截断巨量会破坏量价背离信号。

四、复权与对齐:让价格可比、让多标的同步

复权解决的是除权除息导致的价格跳空。前复权以当前价为基准回溯调整,适合看最新价格形态;后复权以上市价为基准,适合长周期收益计算。建模中若计算收益率,建议统一使用后复权或复权因子,避免前复权随新除权事件变化导致历史数据“漂移”。

对齐解决的是多标的、多频率数据的时间轴一致问题。核心原则:以交易日历为骨架,用 reindex 或 join 对齐,禁止用自然日 resample 直接聚合。多标的对齐后,需检查每列缺失率与首末日期。

# 多标的对齐
symbols = ["600519","000001","300750"]
frames = {s: load_daily(s) for s in symbols}
panel = pd.concat(frames, axis=1, keys=symbols)
panel = panel.reindex(cal)                 # 统一交易日历
panel = panel.ffill(limit=5)               # 短缺口填充
print(panel.isna().mean())                 # 各列缺失率
量价数据清洗管道:从原始接口到建模面板 数据源/API akshare / 交易所 / DB 原始落盘 Parquet 增量存储 缺失值处理 停牌/日历/新股 异常值检测 逻辑+统计双校验 复权处理 前复权/后复权/因子 多标对齐 交易日历 reindex 建模面板 Panel / 因子矩阵 质量校验清单: ① 每列缺失率 < 5% ② 无 high<low 逻辑错误 ③ 复权口径全表统一 ④ 交易日历对齐无自然日 ⑤ 停牌日 is_tradable=False

五、误区与复盘

实践中最高频的三个坑:其一,用自然日 resample('D') 聚合,导致周末产生虚假K线;其二,前复权数据每次除权后历史值变化,回测结果不可复现,应改用后复权或固定复权因子;其三,把停牌日填充价格后直接参与回测,制造了无法成交的“幽灵收益”。

复盘方法:对清洗后的面板做一次“数据体检报告”——统计各标的缺失率、异常值数量、首末交易日、复权方式,并与原始源抽样比对。只有体检通过的数据,才允许进入因子计算与标签构建环节。数据清洗不是一次性任务,而是每次增量更新后都要重跑的流水线。