3. 数据获取与清洗:从原始数据到可用因子
做量化,有一句老话:数据决定上限,策略决定下限。你想想看,再牛的模型,喂进去一堆脏数据,出来的结果能信吗?
这一章,我们就来聊聊数据从哪来、怎么洗、怎么存。我个人习惯把数据获取和清洗看作一个整体流程——拿到手的数据,几乎不可能直接用。
核心观点:数据清洗占整个因子挖掘工作量的 60% 以上。别嫌烦,这一步省下的时间,后面会加倍还回来。
3.1 数据源介绍:选对源头,事半功倍
市面上数据源很多,但真正适合量化研究的,其实就那么几个。我这些年用过不下十种数据源,最后稳定下来的就三个。
| 数据源 | 特点 | 适用场景 | 成本 |
|---|---|---|---|
| Tushare | 国内开源,数据全面,社区活跃 | 个人研究、中小型策略 | 免费/积分制 |
| Wind | 机构级,数据质量高,接口稳定 | 机构量化、实盘交易 | 高(年费数万) |
| Yahoo Finance | 全球数据,免费,但质量参差 | 海外市场、快速原型 | 免费 |
我的建议:刚开始做研究,用 Tushare 就够了。等策略跑通了,要上实盘了,再考虑 Wind。别一上来就花大价钱买数据——你都不知道自己的策略能不能赚钱呢。
3.1.1 Tushare:国内量化首选
Tushare 是我用得最多的数据源。它覆盖了股票、基金、期货、宏观经济等几乎所有国内金融数据。接口设计得也比较合理,用起来顺手。
import tushare as ts
# 设置 token(需要注册获取)
ts.set_token('your_token_here')
pro = ts.pro_api()
# 获取日线数据
df = pro.daily(ts_code='000001.SZ', start_date='20230101', end_date='20231231')
print(df.head())
嗯,这里要注意:Tushare 的积分制度。基础接口免费,但高级数据需要积分。我个人习惯先攒够 2000 积分,基本覆盖大部分需求了。
3.1.2 Wind:机构级数据源
Wind 是机构标配。数据质量确实高,但价格也高。我在上一家私募时,每年光 Wind 数据费就十几万。不过话说回来,它的数据清洗程度确实好,很多字段拿过来就能直接用。
from WindPy import w
w.start()
# 获取日线数据
data = w.wsd("000001.SZ", "open,high,low,close,volume", "2023-01-01", "2023-12-31")
print(data.Data)
避坑提醒:Wind 的 Python 接口在 Mac 上兼容性不太好。我曾经折腾了两天才跑通。建议在 Windows 环境下使用,或者用 Docker 封装一下。
3.1.3 Yahoo Finance:海外市场利器
做海外市场,Yahoo Finance 是绕不开的。免费、数据全,但质量嘛...你懂的。我一般用它来做快速原型验证,正式研究还是会用其他付费数据源。
import yfinance as yf
# 获取苹果公司数据
aapl = yf.Ticker("AAPL")
df = aapl.history(start="2023-01-01", end="2023-12-31")
print(df.head())
3.2 数据清洗实战:脏数据是量化的大敌
数据清洗,说白了就是让数据变得「可用」。我见过太多人,数据拿到手就直接算因子,结果回测曲线漂亮得不行,实盘一跑就崩。为什么?数据没洗干净。
3.2.1 处理缺失值
缺失值是最常见的问题。股票停牌、数据源漏传、节假日...原因很多。处理方式也分几种:
- 直接删除:缺失比例小于 5% 时可用
- 向前填充:用上一个交易日的数据填充(最常用)
- 插值法:线性插值或时间加权插值
- 行业均值:用同行业其他股票的数据填充
import pandas as pd
import numpy as np
# 模拟有缺失值的数据
dates = pd.date_range('2023-01-01', periods=10, freq='D')
data = {
'close': [100, 101, np.nan, 103, 104, np.nan, 106, 107, 108, 109],
'volume': [10000, 11000, 10500, np.nan, 12000, 11500, 13000, 12500, 14000, 13500]
}
df = pd.DataFrame(data, index=dates)
# 向前填充(最常用)
df['close_ffill'] = df['close'].ffill()
# 线性插值
df['close_interp'] = df['close'].interpolate(method='linear')
print(df)
我的经验:对于日线数据,向前填充是最稳妥的。因为股票停牌期间,价格信息其实没有更新,用上一个交易日的数据是合理的。插值法反而可能引入「未来信息」。
3.2.2 处理异常值
异常值比缺失值更隐蔽。一个涨停板的数据被记成了跌停板,这种错误如果不处理,因子计算会完全跑偏。
我常用的方法有三种:
- 标准差法:超过均值 ± 3 倍标准差的数据视为异常
- 百分位法:低于 1% 或高于 99% 分位数的数据截断
- MAD 法:基于中位数的稳健方法,对极端值不敏感
def detect_outliers_zscore(data, threshold=3):
"""基于 Z-score 检测异常值"""
z_scores = np.abs((data - data.mean()) / data.std())
return z_scores > threshold
def detect_outliers_iqr(data):
"""基于 IQR 检测异常值"""
Q1 = data.quantile(0.25)
Q3 = data.quantile(0.75)
IQR = Q3 - Q1
return (data < Q1 - 1.5 * IQR) | (data > Q3 + 1.5 * IQR)
# 示例
returns = df['close'].pct_change().dropna()
outliers = detect_outliers_zscore(returns)
print(f"检测到 {outliers.sum()} 个异常值")
我曾经踩过的坑:有一次做因子分析,回测结果好得离谱。查了两天才发现,是某只股票在除权日的数据没处理,导致收益率计算出现极端值。从那以后,我每次清洗数据都会先跑一遍异常值检测。
3.2.3 复权处理:被忽视的关键步骤
复权处理是数据清洗中最容易被忽视的环节。为什么?因为很多新手根本不知道有这回事。
股票分红、送股、配股都会导致价格跳空。如果不做复权处理,你的收益率计算就是错的。我见过有人用不复权的数据做回测,结果策略在分红日附近表现异常,还以为是发现了什么规律。
| 复权类型 | 说明 | 适用场景 |
|---|---|---|
| 前复权 | 调整历史价格,保持当前价格不变 | 技术分析、因子计算 |
| 后复权 | 调整当前价格,保持历史价格不变 | 长期收益分析 |
| 不复权 | 原始价格,不做任何调整 | 分红套利策略 |
# Tushare 直接提供复权数据
df_adj = pro.daily(ts_code='000001.SZ',
start_date='20230101',
end_date='20231231',
adj='qfq') # 前复权
# 手动复权(如果数据源不提供)
def adjust_price(df, factor_col='adj_factor'):
"""手动前复权"""
df['close_adj'] = df['close'] * df[factor_col]
df['open_adj'] = df['open'] * df[factor_col]
df['high_adj'] = df['high'] * df[factor_col]
df['low_adj'] = df['low'] * df[factor_col]
return df
我的习惯:做因子研究时,一律用前复权数据。这样计算出来的收益率才是真实的。但要注意,前复权可能会导致早期价格出现负值(极端情况),这时候需要检查数据源是否可靠。
3.3 数据存储:选对格式,效率翻倍
数据清洗完了,怎么存?别告诉我你用 CSV。CSV 读起来方便,但做量化研究,数据量一上来,CSV 就是灾难。
我推荐两种格式:HDF5 和 Parquet。它们各有优劣,看场景选择。
| 特性 | HDF5 | Parquet |
|---|---|---|
| 读取速度 | 快(列式读取) | 非常快(列式存储) |
| 压缩率 | 中等 | 高(可配置) |
| 跨语言支持 | 好(Python/R/Matlab) | 非常好(几乎所有大数据框架) |
| 元数据管理 | 内置(类似文件系统) | 需要外部管理 |
| 适用场景 | 中小规模研究 | 大规模生产环境 |
3.3.1 HDF5:研究阶段的利器
HDF5 是我个人最常用的格式。它支持分层存储,可以把不同频率、不同品种的数据放在同一个文件里,管理起来很方便。
import h5py
import pandas as pd
# 写入 HDF5
with h5py.File('market_data.h5', 'w') as f:
# 创建组
f.create_group('stocks')
f.create_group('index')
# 存储数据
f['stocks/000001'] = df.values
f['stocks/000001'].attrs['columns'] = list(df.columns)
f['stocks/000001'].attrs['start_date'] = '2023-01-01'
f['stocks/000001'].attrs['end_date'] = '2023-12-31'
# 读取 HDF5
with h5py.File('market_data.h5', 'r') as f:
data = f['stocks/000001'][:]
columns = f['stocks/000001'].attrs['columns']
df_loaded = pd.DataFrame(data, columns=columns)
我的经验:HDF5 的压缩选项很关键。用 'lzf' 压缩器,速度最快;用 'gzip' 压缩率最高。我一般用 'lzf',因为研究阶段读写的频率远高于存储空间的成本。
3.3.2 Parquet:生产环境的首选
Parquet 是列式存储格式,在大数据生态里用得很多。如果你要处理 TB 级别的数据,或者需要跟 Spark、Flink 等框架配合,Parquet 是不二之选。
import pandas as pd
# 写入 Parquet
df.to_parquet('market_data.parquet',
compression='snappy',
index=False)
# 读取 Parquet
df_loaded = pd.read_parquet('market_data.parquet')
# 按列读取(只读需要的列)
df_partial = pd.read_parquet('market_data.parquet',
columns=['ts_code', 'trade_date', 'close'])
注意:Parquet 的压缩算法要选对。snappy 速度快,gzip 压缩率高。我建议用 snappy,因为量化研究更看重读取速度。另外,Parquet 不支持增量写入,每次写都是全量覆盖,这点跟 HDF5 不同。
3.4 本章小结
数据获取与清洗,是量化研究的基石。选对数据源、洗好数据、存好数据,后面的因子挖掘才能站得住脚。
我个人建议的流程是:先用 Tushare 做研究,数据清洗用向前填充 + 标准差法处理异常值,复权用前复权,存储先用 HDF5 方便调试,等策略成熟了再迁移到 Parquet。
记住一句话:数据质量决定策略质量的上限。别在数据上偷懒,后面会加倍还回来。