第二章:金融市场数据获取——数据源选择、清洗与频率转换
做统计套利,第一步就是搞数据。这活儿看着简单,其实坑特别多。我见过不少新手,策略写得漂漂亮亮,结果一跑回测就崩——查到最后,数据源选错了,或者时间戳没对齐。今天咱们就把这块彻底捋清楚。
2.1 数据源选择:三个主流方案怎么挑?
目前国内做量化,主流数据源就三个:Yahoo Finance、Tushare、Wind。我三个都用过,说说我的真实感受。
| 数据源 | 免费/付费 | 覆盖范围 | 更新频率 | 适合场景 |
|---|---|---|---|---|
| Yahoo Finance | 免费 | 全球股票、ETF、期货 | 延迟15分钟 | 海外市场、快速原型 |
| Tushare | 免费+积分制 | A股、基金、期货、宏观 | 实时/日频 | 国内A股、因子研究 |
| Wind | 付费(较贵) | A股、港股、债券、衍生品 | 实时/高频 | 机构级、高频策略 |
我的建议:个人研究用Tushare就够了。Yahoo Finance适合做海外股票,但数据质量参差不齐。Wind虽然贵,但数据干净,适合机构。我早期做回测时用Yahoo Finance,结果发现某只股票的分红数据缺失,导致回测收益虚高——嗯,从那以后我养成了交叉验证的习惯。
2.2 数据清洗与对齐:最容易被忽视的环节
数据拿到手,千万别直接跑模型。你想想看,不同数据源的时间戳格式可能不一样,有的用UTC,有的用北京时间。还有停牌、涨跌停、复权这些细节,处理不好就是灾难。
2.2.1 时间戳对齐
我习惯把所有时间统一成北京时间,并且只保留交易日。非交易日的脏数据必须剔除。举个例子:
import pandas as pd
# 假设从Tushare拿到数据
df['trade_date'] = pd.to_datetime(df['trade_date'])
df.set_index('trade_date', inplace=True)
# 只保留A股交易日(剔除周末和节假日)
df = df[df.index.isin(trade_calendar)]
避坑指南:我曾经遇到过Tushare的日线数据里混入了非交易日,原因是某只基金在节假日有净值更新。所以别偷懒,一定要用交易日历做过滤。
2.2.2 复权处理
做统计套利,必须用后复权数据。为什么?因为前复权会改变历史价格,导致因子计算失真。我见过有人用前复权算动量因子,结果回测曲线漂亮得不像话——其实是复权方式搞错了。
# Tushare获取后复权数据
df = pro.daily(ts_code='000001.SZ', start_date='20200101',
end_date='20231231', adj='qfq') # qfq=前复权,hfq=后复权
注意:后复权数据在除权除息日会有跳空,这是正常的。但如果你做高频策略,建议用原始价格自己算复权因子,别直接用API返回的数据。
2.3 缺失值处理:别让NaN毁了你的模型
金融数据缺失太常见了。停牌、新股上市、退市,都会产生缺失值。我的处理原则是:能补则补,不能补就删。
2.3.1 常见处理方法
- 向前填充(ffill):适合停牌数据,用最近交易日价格填充。我一般用这个。
- 线性插值:适合短期缺失,比如某天数据没更新。
- 直接删除:如果缺失超过20%,建议直接删掉这只股票。
# 向前填充停牌数据
df.fillna(method='ffill', inplace=True)
# 或者用线性插值
df.interpolate(method='linear', inplace=True)
我的经验:做因子挖掘时,千万别用均值填充。金融数据不是正态分布,均值填充会引入偏差。我曾经用均值填充处理过某只小盘股的缺失值,结果因子IC值直接崩了——后来改成ffill才恢复正常。
2.4 数据频率转换:从日频到周频、月频
统计套利常用日频数据,但有些因子需要周频或月频。频率转换的核心是重采样(resample)。这里有个细节:价格用收盘价,成交量用求和,收益率用复利计算。
# 日频转周频
weekly_df = df.resample('W').agg({
'close': 'last', # 周收盘价
'volume': 'sum', # 周成交量
'return': lambda x: (1 + x).prod() - 1 # 周收益率
})
小技巧:转频率时,记得先检查数据是否完整。如果某周只有两个交易日,那周收盘价可能失真。我一般要求每周至少3个交易日才保留。
2.5 本章知识体系总览
下面这张图,是我做数据预处理时的核心流程。每次拿到新数据,我都会按这个步骤走一遍。
说白了,数据预处理占整个量化策略开发时间的60%以上。别嫌麻烦,数据干净了,模型才能跑得稳。我见过太多人花90%时间调模型参数,结果数据一换就崩——嗯,这就是典型的「垃圾进,垃圾出」。
最后提醒一句:每次拿到新数据,先画个分布图看看。异常值、缺失模式、时间跨度,一眼就能看出来。别上来就写代码,先理解数据长什么样。