2. 数据获取与清洗:用akshare搞定ETF行情数据
做量化交易,有一句老话叫「垃圾进,垃圾出」。
你算法再牛,模型再炫,数据源有问题,结果全是废的。我个人习惯,花在数据清洗上的时间,至少占整个策略开发的40%。
这一章,咱们就聊聊怎么用 akshare 把ETF行情数据拿下来,然后把它收拾得干干净净。
2.1 为什么选akshare?
市面上能拿A股数据的库不少,但 akshare 是我用得最顺手的。原因很简单:
- 免费:不用花钱买数据接口,对个人开发者极其友好
- 全:股票、ETF、期货、基金,基本你能想到的国内金融数据它都有
- 更新快:我遇到过几次交易所接口变动,akshare 基本一两天就跟着更新了
pip install akshare 就行。不过要注意,akshare 依赖的 pandas 版本最好别太老,我建议用 pandas 1.3.0 以上。
2.2 获取ETF日线行情
先看最基础的操作——拿日线数据。
import akshare as ak
import pandas as pd
# 获取沪深300 ETF(510300)的日线数据
# 注意:akshare 的 fund_etf_hist_em 接口返回的是前复权数据
df = ak.fund_etf_hist_em(
symbol="510300",
period="daily",
start_date="20200101",
end_date="20231231",
adjust="qfq" # qfq=前复权, hfq=后复权
)
print(df.head())
print(df.columns.tolist())
跑完这段代码,你会看到类似这样的输出:
| 日期 | 开盘 | 收盘 | 最高 | 最低 | 成交量 | 成交额 |
|---|---|---|---|---|---|---|
| 2020-01-02 | 3.892 | 3.912 | 3.918 | 3.882 | 125678900 | 4.91e8 |
| 2020-01-03 | 3.905 | 3.885 | 3.915 | 3.878 | 98765400 | 3.84e8 |
嗯,这里要注意:akshare 返回的日期列是 object 类型,不是 datetime。我建议你第一时间把它转成时间格式:
df['日期'] = pd.to_datetime(df['日期'])
df.set_index('日期', inplace=True)
df.sort_index(inplace=True) # 确保按时间升序排列
2.3 处理缺失值——别小看这个步骤
为什么会有缺失值?
- ETF 本身有停牌日
- 节假日不同(比如港股通ETF,港股休市但A股开市)
- 数据源本身就有遗漏
我曾经在回测一个ETF轮动策略时,因为没处理缺失值,结果策略在某个停牌日「凭空」交易了一次,回测收益直接虚高了5%。
处理缺失值,我一般分三步走:
- 先检查:看看缺失值到底有多少
- 再判断:是零星缺失还是大段缺失
- 最后填充:选合适的方法
# 第一步:检查缺失值
print(df.isnull().sum())
# 第二步:查看缺失值的位置
missing_dates = df[df.isnull().any(axis=1)]
print(missing_dates)
# 第三步:填充策略
# 对于ETF日线,我通常用前向填充(ffill)
# 因为停牌日的价格,理论上等于上一个交易日
df.fillna(method='ffill', inplace=True)
# 如果开头就有缺失,用后向填充
df.fillna(method='bfill', inplace=True)
dropna() 直接删行!你想想看,删除一行数据,你的时间序列就「断档」了。后面做技术指标计算时,窗口函数会出问题。
2.4 复权处理——为什么必须做?
ETF会分红,会拆分。如果不做复权处理,你的K线图上会出现莫名其妙的跳空缺口。
说白了,复权就是「抹平」这些非交易因素造成的价格变动,让价格序列连续。
akshare 的 adjust 参数提供了三种选择:
| 参数值 | 含义 | 适用场景 |
|---|---|---|
""(空) |
不复权 | 看真实历史价格 |
"qfq" |
前复权 | 回测、技术分析(推荐) |
"hfq" |
后复权 | 长期收益分析 |
我个人习惯用 前复权。为什么?因为前复权调整的是历史价格,当前价格是真实的。你回测时,买入卖出信号都是基于「当前能看到的价格」触发的,更贴近实盘。
但要注意一点:前复权后的价格可能变成负数(极端情况下)。不过ETF很少出现这种情况,股票倒是偶尔会有。
2.5 数据对齐——多ETF一起分析时的必修课
做择时信号,很少只盯一只ETF。比如你想比较沪深300和创业板50的强弱,就需要把两只ETF的数据对齐到同一个时间轴上。
对齐的核心问题:交易日不一致。
- 沪深300 ETF:A股交易日
- 恒生ETF:港股交易日(和A股不完全一样)
- 标普500 ETF:美股交易日(差别更大)
我的做法是这样的:
# 假设 df_hs300 和 df_cyb50 是两个DataFrame
# 先获取它们的并集日期
all_dates = df_hs300.index.union(df_cyb50.index)
# 重新索引,缺失值用NaN填充
df_hs300_aligned = df_hs300.reindex(all_dates)
df_cyb50_aligned = df_cyb50.reindex(all_dates)
# 用前向填充处理缺失值
df_hs300_aligned.fillna(method='ffill', inplace=True)
df_cyb50_aligned.fillna(method='ffill', inplace=True)
# 最后只保留两者都有数据的日期
common_dates = df_hs300.index.intersection(df_cyb50.index)
df_hs300_final = df_hs300_aligned.loc[common_dates]
df_cyb50_final = df_cyb50_aligned.loc[common_dates]
2.6 知识体系总览
下面这张图,把数据获取与清洗的整个流程串起来了:
2.7 完整代码示例
把上面所有步骤串起来,就是一个完整的数据获取与清洗函数:
def get_etf_data(symbol, start_date, end_date):
"""
获取并清洗ETF日线数据
"""
# 1. 获取数据
df = ak.fund_etf_hist_em(
symbol=symbol,
period="daily",
start_date=start_date,
end_date=end_date,
adjust="qfq"
)
# 2. 日期处理
df['日期'] = pd.to_datetime(df['日期'])
df.set_index('日期', inplace=True)
df.sort_index(inplace=True)
# 3. 只保留需要的列
df = df[['开盘', '收盘', '最高', '最低', '成交量', '成交额']]
# 4. 处理缺失值
df.fillna(method='ffill', inplace=True)
df.fillna(method='bfill', inplace=True)
# 5. 删除最后可能残留的NaN
df.dropna(inplace=True)
return df
# 使用示例
hs300 = get_etf_data("510300", "20200101", "20231231")
cyb50 = get_etf_data("159949", "20200101", "20231231")
ak.fund_etf_hist_em 的 start_date 参数确认一下ETF的上市日期。
好了,数据准备好了。下一章咱们就用这些干净的数据,开始计算真正的择时信号。
公众号:蓝海资料掘金营,微信deep3321