第二章:金融数据获取——从零开始搭建你的数据管道
做量化研究,第一步永远绕不开数据。没有数据,再漂亮的模型也只是空中楼阁。今天我们就来聊聊,怎么用 Python 把股票数据拿到手,并且把它收拾得服服帖帖。
2.1 为什么选 pandas-datareader?
市面上获取金融数据的工具不少,但我个人最常用的是 pandas-datareader。原因很简单:它跟 pandas 无缝衔接,拿回来的数据直接就是 DataFrame,省去了大量格式转换的麻烦。
你可能会问:「为什么不直接用 tushare 或者 baostock?」嗯,这个问题问得好。那些工具确实也不错,但 pandas-datareader 最大的优势在于——它支持多个数据源。雅虎财经、FRED、世界银行……一个接口全搞定。我在做跨国资产配置研究时,这个特性帮了大忙。
核心优势:
- 与 pandas 深度集成,返回 DataFrame
- 支持多数据源切换(Yahoo、Google、FRED 等)
- 代码简洁,三行就能拿到数据
- 社区活跃,遇到问题容易找到解决方案
2.2 安装与基础配置
先装包,这个不用多说。但我建议你用虚拟环境,别把全局环境搞乱了。我曾经有一次因为包版本冲突,调试了整整一个下午……
# 推荐使用 conda 或 venv 创建虚拟环境
pip install pandas-datareader
pip install pandas numpy matplotlib # 顺便装上常用库
装好之后,验证一下:
import pandas_datareader as pdr
import pandas as pd
import numpy as np
from datetime import datetime
print(pdr.__version__) # 看看版本,我目前用的是 0.10.0
2.3 获取股票数据——实战操作
好了,开始干活。我们以苹果公司(AAPL)为例,获取它最近一年的日线数据。
# 定义时间范围
start_date = '2023-01-01'
end_date = '2023-12-31'
# 获取数据
df = pdr.get_data_yahoo('AAPL', start=start_date, end=end_date)
# 看一眼数据长什么样
print(df.head())
print(df.info())
拿到数据后,你会看到这样的结构:
| 字段 | 说明 |
|---|---|
| Open | 开盘价 |
| High | 最高价 |
| Low | 最低价 |
| Close | 收盘价(我们最常用的) |
| Volume | 成交量 |
| Adj Close | 复权收盘价(做回测时一定要用这个) |
避坑指南:我曾经直接用 Close 做回测,结果发现分红除权后收益率算出来全是错的。记住,做因子研究时永远用 Adj Close,除非你明确知道自己在干什么。
2.4 数据清洗——把脏数据洗干净
真实世界的数据从来不会干干净净。你拿到的数据可能有缺失值、有异常值、甚至日期都不连续。我见过最离谱的一次,某只股票的数据里居然出现了 2023年2月30日……
清洗步骤一般包括:
- 检查缺失值——用
isnull().sum()看看哪些列有空值 - 处理缺失值——前向填充、后向填充、或者直接删除
- 检查日期连续性——交易日历要对得上
- 去重——有时候同一个交易日会出现两条数据
# 检查缺失值
print(df.isnull().sum())
# 前向填充(用前一天的数据补今天的缺失)
df.fillna(method='ffill', inplace=True)
# 检查日期是否连续
date_range = pd.date_range(start=df.index.min(), end=df.index.max(), freq='B')
missing_dates = date_range.difference(df.index)
print(f'缺失的交易日数量:{len(missing_dates)}')
小技巧:如果你发现缺失的交易日正好是节假日,那就不用管。但如果是工作日缺失,就要警惕了——可能是停牌或者数据源出了问题。
2.5 数据对齐——让多只股票步调一致
做因子模型时,你通常需要同时处理多只股票。问题来了:每只股票的交易日期可能不一样。A股有节假日调休,美股有自己的一套假期,港股更是复杂。
数据对齐的核心思想很简单:取所有股票交易日的交集。说白了,就是只保留大家都交易的日子。
# 获取多只股票数据
tickers = ['AAPL', 'MSFT', 'GOOGL']
data_dict = {}
for ticker in tickers:
data_dict[ticker] = pdr.get_data_yahoo(ticker, start=start_date, end=end_date)['Adj Close']
# 合并成一个 DataFrame
price_df = pd.DataFrame(data_dict)
# 删除任何包含 NaN 的行(这就是对齐)
price_df.dropna(inplace=True)
print(price_df.head())
这样做的好处是,后续计算协方差矩阵、回归分析时,不会因为日期不匹配而出错。坏处是——如果某只股票停牌时间太长,你会损失很多数据点。这时候就要权衡了,我一般会设定一个阈值:如果某只股票缺失超过 20% 的数据,我就直接把它剔除。
2.6 收益率计算——从价格到收益
价格本身没什么意义,我们真正关心的是收益率。计算收益率有两种常用方法:
- 简单收益率:(P_t - P_{t-1}) / P_{t-1}
- 对数收益率:ln(P_t / P_{t-1})
我个人偏好对数收益率,原因有三:第一,它近似正态分布,方便做统计推断;第二,它具有时间可加性,多期收益率可以直接相加;第三,数值稳定性更好,不会出现收益率超过 100% 的尴尬情况。
# 计算对数收益率
returns = np.log(price_df / price_df.shift(1))
# 删除第一行(NaN)
returns = returns.dropna()
print(returns.head())
注意:shift(1) 表示把数据向下移动一行。这样今天的价格除以上一天的价格,就得到了今天的收益率。如果你用 shift(-1),那就变成未来收益率了——做回测时千万别搞反,我见过有人因为这个错误亏了不少模拟资金。
2.7 描述性统计——给你的数据做个体检
拿到收益率数据后,先别急着建模。先做个「体检」,看看数据的基本特征。
# 描述性统计
stats = returns.describe().T
stats['skew'] = returns.skew() # 偏度
stats['kurtosis'] = returns.kurtosis() # 峰度
stats['jarque_bera'] = returns.apply(lambda x: __import__('scipy.stats').stats.jarque_bera(x.dropna())[0])
print(stats.round(4))
你会看到类似这样的结果:
| 指标 | 含义 | 正常范围 |
|---|---|---|
| mean | 平均收益率 | 一般接近 0 |
| std | 波动率(风险) | 股票通常 0.01~0.03 |
| skew | 偏度(对称性) | 接近 0 为对称 |
| kurtosis | 峰度(尾部厚度) | 正态分布为 3 |
如果偏度绝对值大于 1,或者峰度远大于 3,说明你的数据有「肥尾」特征——这在金融数据里很常见,但做因子模型时要注意,很多经典模型假设收益率是正态分布的,实际数据往往不满足。
2.8 本章知识体系总览
下面这张图,是我自己梳理的数据处理流程。你照着这个顺序做,基本不会出错。
这张图里有个关键点我想强调一下:数据质量检查不是一次性的。做完描述性统计后,如果发现偏度异常或者波动率离谱,一定要回到清洗步骤重新检查。我见过太多人做完描述性统计就直接建模了,结果模型跑出来一塌糊涂——回头一看,原来是数据里混进了停牌期间的价格。
2.9 避坑总结
最后,分享几个我踩过的坑:
- 不要用收盘价直接算收益率——一定要用复权价,否则分红除权会扭曲结果
- 注意时区问题——雅虎财经返回的是美国东部时间,如果你跟国内数据混用,日期会错位
- 数据源可能随时失效——雅虎财经的接口就曾经停用过,建议同时准备一个备用数据源
- 别一次性拉太多数据——我试过一次性拉 5000 只股票 10 年的数据,结果被雅虎封了 IP……
好了,数据准备的工作就到这里。数据干净了,下一步才能安心做因子分析。记住一句话:垃圾进,垃圾出。数据质量决定了你模型的上限。