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%。

处理缺失值,我一般分三步走:

  1. 先检查:看看缺失值到底有多少
  2. 再判断:是零星缺失还是大段缺失
  3. 最后填充:选合适的方法
# 第一步:检查缺失值
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]
核心原则: 宁可少一些数据,也不要「伪造」数据。如果某只ETF停牌超过5个交易日,我建议直接跳过那段时间,不要强行填充。

2.6 知识体系总览

下面这张图,把数据获取与清洗的整个流程串起来了:

ETF数据获取与清洗流程 1. 数据获取 akshare.fund_etf_hist_em 2. 缺失值处理 检查 → 判断 → 填充 3. 复权处理 前复权/后复权 4. 数据对齐 多ETF交易日对齐 → 前向填充 ✅ 干净、连续、对齐的ETF行情数据 停牌日、节假日 分红、拆分 用于择时信号计算

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")
避坑指南: 我曾经在获取多只ETF数据时,发现某只ETF在某个时间段内完全没有数据。后来查了才知道,那只ETF是后来才上市的。所以建议你在获取数据前,先用 ak.fund_etf_hist_emstart_date 参数确认一下ETF的上市日期。

好了,数据准备好了。下一章咱们就用这些干净的数据,开始计算真正的择时信号。


公众号:蓝海资料掘金营,微信deep3321