第四章:数据获取与清洗——金融Agent的“食材准备”
做量化回测,数据就是你的命根子。我见过太多人,策略写得天花乱坠,结果数据一塌糊涂,回测结果根本没法看。说白了,数据获取与清洗,就是给金融Agent准备食材——食材不新鲜,厨艺再好也白搭。
这一章,咱们就聊聊怎么用yfinance拿数据,怎么把脏数据洗干净,怎么对齐不同频率的数据。嗯,都是我在实战中踩过的坑,今天一并倒给你。
4.1 用yfinance获取股票数据
yfinance这个库,我用了好几年了。它免费、简单,而且数据质量对于回测来说基本够用。我个人习惯是把它当作数据源的第一道关口。
先装库:
pip install yfinance pandas numpy
然后看个例子:
import yfinance as yf
import pandas as pd
# 获取贵州茅台的历史数据
ticker = "600519.SS"
data = yf.download(ticker, start="2020-01-01", end="2023-12-31")
print(data.head())
这里要注意,A股代码后面要加后缀。上海交易所是.SS,深圳交易所是.SZ。我曾经因为忘了加后缀,查了半天数据都是空的,后来才发现是代码写错了。
如果你需要多只股票,可以这样:
tickers = ["600519.SS", "000001.SZ", "601318.SS"]
data = yf.download(tickers, start="2020-01-01", end="2023-12-31")
# 这会返回一个MultiIndex的DataFrame
close_prices = data['Adj Close']
print(close_prices.head())
你想想看,一次拿几十只股票的数据,几秒钟就搞定了。这在以前,得手动从各种网站爬,效率天差地别。
4.2 数据清洗——缺失值与异常值
数据拿到手,别急着用。先看看有没有坑。我遇到过最离谱的一次,某只股票连续三天收盘价都是同一个数字,一看就是数据源出了问题。
4.2.1 缺失值处理
金融数据里,缺失值很常见。比如节假日、停牌、数据源中断等。我们先检查一下:
# 检查缺失值
print(data.isnull().sum())
# 查看缺失值比例
missing_ratio = data.isnull().mean()
print(missing_ratio[missing_ratio > 0])
处理缺失值,我一般用这几种方法:
| 方法 | 适用场景 | 代码示例 |
|---|---|---|
| 向前填充 | 停牌、节假日(用上一个交易日数据) | data.fillna(method='ffill') |
| 向后填充 | 数据延迟(用下一个交易日数据) | data.fillna(method='bfill') |
| 插值法 | 少量缺失,且趋势明显 | data.interpolate() |
| 直接删除 | 缺失比例过高(比如超过20%) | data.dropna() |
我个人习惯是:对于日线数据,先用向前填充。因为停牌后复牌,价格通常不会跳空太多。但如果是分钟级数据,我会用插值法,因为高频数据对连续性要求更高。
4.2.2 异常值处理
异常值,说白了就是那些明显不合理的数据。比如某只股票突然涨了1000%,或者成交量突然变成0。这些通常都是数据录入错误。
我常用的检测方法:
# 计算日收益率
returns = data['Adj Close'].pct_change()
# 找出收益率绝对值超过20%的日期
outliers = returns[abs(returns) > 0.2]
print(outliers)
对于A股,单日涨跌幅限制是10%(ST股是5%)。如果出现超过这个范围的,基本可以判定是异常值。处理方式:
- 替换为NaN,然后用缺失值处理方法填充
- 用前后两天的均值替换
- 直接删除该行数据(如果异常值不多)
# 将异常值替换为NaN
data.loc[abs(returns) > 0.2, 'Adj Close'] = None
# 然后用向前填充
data['Adj Close'] = data['Adj Close'].fillna(method='ffill')
4.3 数据对齐与重采样
做多资产回测时,不同股票的交易日期可能不一样。比如A股和港股,节假日不同,数据长度就不一致。这时候就需要对齐。
4.3.1 数据对齐
对齐的核心思想是:只保留所有股票都有数据的日期。
# 假设我们有两只股票的数据
df1 = yf.download("600519.SS", start="2020-01-01", end="2023-12-31")['Adj Close']
df2 = yf.download("000001.SZ", start="2020-01-01", end="2023-12-31")['Adj Close']
# 合并并自动对齐
combined = pd.concat([df1, df2], axis=1, keys=['茅台', '平安'])
# 删除任何一行有缺失的数据
combined_aligned = combined.dropna()
print(combined_aligned.head())
这里有个坑:如果某只股票停牌时间很长,dropna会删掉大量数据。我建议先看看缺失比例,如果超过10%,可以考虑用填充而不是删除。
4.3.2 重采样
有时候我们需要把日线数据转换成周线或月线。重采样就是干这个的。
# 将日线重采样为周线
weekly_data = data['Adj Close'].resample('W').last()
# 将日线重采样为月线
monthly_data = data['Adj Close'].resample('M').last()
# 计算周收益率
weekly_returns = weekly_data.pct_change()
重采样的频率参数:
| 参数 | 含义 |
|---|---|
| W | 周(默认周日为结束) |
| M | 月(月末) |
| Q | 季度 |
| Y | 年 |
| B | 工作日 |
你想想看,做长周期策略时,用日线数据计算信号,计算量巨大。重采样成周线或月线,效率能提升好几倍。
4.4 本章知识体系
下面这张图,是我自己总结的数据处理流程。你照着这个顺序做,基本不会出大问题。
嗯,这张图把整个流程串起来了。你照着这个顺序做,基本不会漏掉关键步骤。我个人习惯是把每一步都写成函数,然后串成一个pipeline,这样每次跑新数据,一行代码就搞定了。
数据清洗这件事,说白了就是“垃圾进,垃圾出”。你花80%的时间在数据上,剩下20%的时间写策略,这才是正确的比例。别想着一步到位,数据这东西,越洗越干净。