数据获取与预处理:用akshare搞定基金净值数据
做基金组合量化调仓,第一步就是拿数据。这一步看着简单,其实坑不少。我刚开始做量化的时候,就吃过数据不干净的亏——回测跑得漂漂亮亮,实盘一跑就崩。后来才明白,数据预处理比策略本身还重要。
今天我们就聊聊怎么用akshare获取基金净值数据,以及怎么把数据洗干净、对齐好。
1. 用akshare获取基金净值数据
akshare这个库,我个人很喜欢。它把很多金融数据接口封装好了,用起来特别顺手。获取基金净值,主要用 fund_etf_hist_em 这个函数。
先装好库:
pip install akshare pandas numpy
然后写个简单的获取函数:
import akshare as ak
import pandas as pd
def get_fund_nav(fund_code, start_date="20200101", end_date="20241231"):
"""
获取单只基金净值数据
"""
df = ak.fund_etf_hist_em(
symbol=fund_code,
period="daily",
start_date=start_date,
end_date=end_date,
adjust="qfq" # 前复权
)
return df
# 举个例子:获取沪深300ETF(510300)的数据
df_300 = get_fund_nav("510300")
print(df_300.head())
这里有个细节——复权处理。我建议用前复权,因为分红、拆分会造成净值跳空,前复权能把历史数据调整到当前视角,回测更真实。
小技巧: 如果你要获取多只基金,别一个个请求。写个循环批量拉取,但记得加 time.sleep(1) 避免被限流。我曾经一次性拉100只基金,结果被服务器封了IP半小时...
2. 数据清洗——把脏数据洗白
拿到数据后,你会发现很多问题。比如:
- 日期格式不统一
- 列名是中文,不方便处理
- 有些基金净值是0或者空值
- 停牌期间数据缺失
我一般这样处理:
def clean_fund_data(df):
"""
清洗基金净值数据
"""
# 重命名列
df.columns = ['date', 'open', 'close', 'high', 'low', 'volume', 'amount', 'amplitude', 'pct_change', 'change', 'turnover']
# 日期转标准格式
df['date'] = pd.to_datetime(df['date'])
# 按日期排序
df = df.sort_values('date').reset_index(drop=True)
# 删除净值<=0的异常行
df = df[df['close'] > 0]
# 删除缺失值
df = df.dropna(subset=['close', 'open', 'high', 'low'])
return df
df_clean = clean_fund_data(df_300)
嗯,这里要注意:千万别直接 dropna 全部干掉。有些缺失值是正常的,比如节假日。我一般先看看缺失比例,超过5%才考虑删除或填充。
3. 数据对齐——让多只基金站在同一起跑线
做组合调仓,你得让所有基金的数据在时间轴上对齐。比如你有5只基金,有的从2018年开始有数据,有的从2020年开始。直接合并会出问题。
我的做法是:
def align_fund_data(fund_dict, start_date="20200101"):
"""
对齐多只基金数据
fund_dict: {基金代码: 清洗后的DataFrame}
"""
# 提取净值列,重命名为基金代码
nav_series = {}
for code, df in fund_dict.items():
nav_series[code] = df.set_index('date')['close']
# 合并成一个DataFrame
nav_df = pd.DataFrame(nav_series)
# 只保留共同有数据的日期
nav_df = nav_df.dropna(how='any')
# 或者用前向填充,保留更多数据
# nav_df = nav_df.fillna(method='ffill').dropna()
return nav_df
# 假设有3只基金
fund_codes = ["510300", "510500", "159915"]
fund_dict = {}
for code in fund_codes:
df = get_fund_nav(code)
df = clean_fund_data(df)
fund_dict[code] = df
aligned_df = align_fund_data(fund_dict)
print(aligned_df.head())
避坑指南: 我曾经用 dropna(how='any') 直接干掉所有缺失行,结果发现数据从1000天变成了300天。后来改用前向填充(ffill),保留了更多有效数据。记住:对齐不是越严格越好,要看你的策略需求。
4. 处理缺失值与异常值
数据对齐后,可能还有零星的缺失值。常见处理方法:
| 方法 | 适用场景 | 代码示例 |
|---|---|---|
| 前向填充 | 短期停牌、节假日 | df.fillna(method='ffill') |
| 线性插值 | 少量缺失,趋势明显 | df.interpolate(method='linear') |
| 删除行 | 缺失比例 < 1% | df.dropna() |
| 均值填充 | 不推荐用于时间序列 | df.fillna(df.mean()) |
异常值处理更关键。比如某天净值突然涨了20%,明显是数据错误。我一般用 3σ原则 或 IQR方法 来检测:
def detect_outliers(series, method='iqr'):
"""
检测异常值
"""
if method == 'iqr':
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return (series < lower) | (series > upper)
elif method == 'zscore':
from scipy import stats
z = np.abs(stats.zscore(series))
return z > 3
# 应用到收益率序列
returns = aligned_df.pct_change().dropna()
outliers = detect_outliers(returns['510300'])
print(f"发现 {outliers.sum()} 个异常值")
发现异常值后,我一般用前一天的净值替换,或者直接删除。千万别用均值填充——时间序列的均值填充会破坏自相关性。
5. 知识体系总览
下面这张图,是我做数据预处理的核心流程。你想想看,数据从原始状态到可用的量化数据,中间要经过多少道工序:
6. 实战中的几个坑
最后分享几个我踩过的坑:
- 基金代码搞混:ETF基金和场外基金代码不同,别混用。我一开始用510300的场外代码去拉数据,结果啥也没拉到。
- 复权方式选错:回测用前复权,实盘用后复权。搞反了收益率会算错。
- 节假日处理:A股有春节、国庆长假,数据会断档。对齐时别用连续日期,要用交易日历。
- 数据更新延迟:akshare的数据一般T+1更新,当天收盘后要等第二天才能拿到完整数据。
核心原则: 数据预处理的目标不是让数据变得完美,而是让数据变得可用。接受一定程度的不完美,但要确保你的策略对数据质量有鲁棒性。
好了,数据获取和预处理就聊到这儿。下一节我们会用这些干净的数据,开始做真正的调仓策略。记住:数据质量决定了策略的上限,策略本身只是把数据价值变现的工具。