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())
二、缺失值处理:先分类,再决策
行情缺失不是随机噪声,而是有明确成因。常见有四类:停牌(无成交,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()) # 各列缺失率
五、误区与复盘
实践中最高频的三个坑:其一,用自然日 resample('D') 聚合,导致周末产生虚假K线;其二,前复权数据每次除权后历史值变化,回测结果不可复现,应改用后复权或固定复权因子;其三,把停牌日填充价格后直接参与回测,制造了无法成交的“幽灵收益”。
复盘方法:对清洗后的面板做一次“数据体检报告”——统计各标的缺失率、异常值数量、首末交易日、复权方式,并与原始源抽样比对。只有体检通过的数据,才允许进入因子计算与标签构建环节。数据清洗不是一次性任务,而是每次增量更新后都要重跑的流水线。