3. 数据获取与预处理实战
量价共振回测的第一道门槛不在策略逻辑,而在数据。跨板块共振要求同一时间截面上同时观察多个板块的指数价格与成交量,如果各板块的交易日历、复权口径、成交量单位不一致,后续计算出的“共振”很可能只是数据错位造成的假象。本节围绕一个可落地的流程展开:先明确需要哪些字段,再获取数据,然后完成清洗、对齐与缺失值处理,最终产出一张可直接送入回测引擎的宽表数据集。
一、明确数据需求:字段、频率与板块范围
跨板块量价共振的最小数据集包含四类信息:时间戳、板块标识、价格序列、成交量序列。价格建议同时保留收盘价与复权因子,成交量保留原始成交量与成交额。频率上,日频是最常用的起点,若要做盘中共振则需分钟频,但分钟频的缺失与对齐问题会显著放大,建议先以日频跑通全流程。
板块范围要提前锁定,例如选择申万一级行业中的银行、券商、半导体、医药、电力五个板块,分别对应不同的市场驱动逻辑。每个板块用其指数代码作为唯一标识,避免用中文名称做键,防止后续合并时因名称差异导致匹配失败。字段规划可参考下表:
| 字段名 | 含义 | 类型 | 处理要点 |
|---|---|---|---|
| trade_date | 交易日期 | 日期 | 统一为 YYYY-MM-DD,作为对齐主键 |
| sector_code | 板块指数代码 | 字符串 | 唯一标识,不用中文名做键 |
| close | 收盘价 | 浮点 | 确认是否为复权价 |
| adj_factor | 复权因子 | 浮点 | 用于还原前复权/后复权价格 |
| volume | 成交量 | 浮点 | 确认单位是股还是手 |
| amount | 成交额 | 浮点 | 用于交叉校验成交量合理性 |
二、数据获取:接口调用与落盘
实际获取时,常见做法是通过行情数据接口按板块代码批量拉取日线。以 Python 为例,假设使用某行情库的指数日线接口,核心逻辑是循环板块代码、逐一下载、统一列名后追加到列表,最后合并为一个长表。下面是一段示意代码,接口名与参数需按实际数据源调整:
import pandas as pd
sector_codes = ["801780", "801790", "801080", "801150", "801160"]
frames = []
for code in sector_codes:
df = fetch_index_daily(code, start="2020-01-01", end="2024-12-31")
df = df.rename(columns={
"date": "trade_date", "close": "close",
"vol": "volume", "amount": "amount"
})
df["sector_code"] = code
frames.append(df[["trade_date", "sector_code", "close", "volume", "amount"]])
raw = pd.concat(frames, ignore_index=True)
raw.to_parquet("raw_sector_daily.parquet", index=False)
落盘时优先选 Parquet 而非 CSV,前者保留数据类型、体积更小、读取更快。原始数据必须原样保存一份,后续所有清洗都在副本上进行,保证可追溯。获取完成后先做一次概览检查:每个板块的行数、日期起止、是否存在重复日期、成交量是否有零值或负值。
三、数据清洗:去重、类型统一与异常值识别
清洗的第一步是去重。同一板块同一交易日出现多条记录,通常来自接口分页重复或多次追加,处理方式是按 trade_date + sector_code 去重并保留最后一条。第二步是类型统一:日期转为 datetime,价格与成交量转为浮点,板块代码转为字符串。第三步是异常值识别,重点看三类情况:收盘价小于等于零、成交量为零但成交额大于零、单日涨跌幅超过板块指数合理阈值(例如超过 11% 需人工核查是否为除权或数据错误)。
成交量单位是最容易被忽略的坑。不同数据源可能用“股”或“手”,若某板块用股、另一板块用手,直接比较成交量大小会失真。处理办法是用成交额除以成交量得到隐含均价,再与收盘价对比:若隐含均价约为收盘价的百分之一,说明成交量单位是手,需乘以 100 统一为股。这一步应在合并前对每个板块单独校验。
四、时间对齐:以交易日历为基准构建宽表
跨板块对齐的核心是确定一个主交易日历。最稳妥的做法是取所有板块交易日的并集作为主索引,再按板块透视成宽表。这样即使某板块因停牌或数据缺失少了某天,也不会丢失其他板块当天的信息。对齐后的结构应为:行是交易日,列是各板块的收盘价与成交量,形成 MultiIndex 列或分开两张宽表。
raw["trade_date"] = pd.to_datetime(raw["trade_date"])
raw = raw.drop_duplicates(["trade_date", "sector_code"], keep="last")
close_wide = raw.pivot(index="trade_date", columns="sector_code", values="close")
vol_wide = raw.pivot(index="trade_date", columns="sector_code", values="volume")
calendar = close_wide.index.union(vol_wide.index).sort_values()
close_wide = close_wide.reindex(calendar)
vol_wide = vol_wide.reindex(calendar)
对齐后要检查两件事:一是各列的非空数量是否接近,若某板块缺失比例明显偏高,需回到原始数据排查是接口漏拉还是该板块确实长期停牌;二是首尾日期是否一致,避免某板块数据起始晚于其他板块导致回测前期出现大量空值。
五、缺失值处理:区分“真缺失”与“非交易日”
缺失值不能一律填充。先判断缺失原因:若某天是主日历中的交易日但该板块无数据,可能是停牌,此时价格应前向填充、成交量填零,因为停牌期间没有真实成交;若缺失发生在板块数据起始日之前,则属于结构性缺失,应将该板块在回测中的可用起点后移,而不是用未来数据回填。前向填充要限制连续填充天数,例如连续超过 5 个交易日缺失就标记为可疑并输出清单人工核查。
close_filled = close_wide.ffill(limit=5)
vol_filled = vol_wide.fillna(0)
missing_report = close_wide.isna().sum()
print(missing_report)
处理完成后做一次一致性校验:用成交额除以成交量得到的均价,与收盘价的偏离度应在合理范围内;各板块日收益率分布不应出现极端离群点;宽表行数应等于主交易日历长度。校验通过后,将收盘价宽表、成交量宽表与交易日历一并保存,回测引擎直接读取即可。
六、交付物:一份可复用的预处理检查清单
为了让每次新增板块或更新数据时都能稳定复现,建议把上述关键判断固化为一份检查清单,随数据集一起保存。清单字段不必多,但要覆盖最容易出错的环节:
- 原始数据是否原样落盘,清洗是否在副本上进行;
- 每个板块的成交量单位是否已通过“成交额/成交量≈收盘价”校验;
- 复权口径是否统一,是否保留复权因子;
- 去重键是否为
trade_date + sector_code; - 主交易日历是否为各板块并集,宽表行数是否与日历一致;
- 缺失值是否区分停牌与结构性缺失,前向填充是否限制天数;
- 各板块缺失比例、首尾日期是否记录在案。
完成以上步骤后,得到的是三份可直接使用的文件:收盘价宽表、成交量宽表、交易日历。后续计算板块收益率、成交量均线、共振信号时,只需在这三份文件上做列运算,不必再回头处理原始数据。数据层的稳定性,是跨板块量价共振回测能够被信任的前提。