数据获取与预处理:用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. 知识体系总览

下面这张图,是我做数据预处理的核心流程。你想想看,数据从原始状态到可用的量化数据,中间要经过多少道工序:

基金净值数据预处理流程 步骤1:数据获取 akshare 批量拉取 步骤2:数据清洗 去重、去空、格式化 步骤3:数据对齐 时间轴统一 步骤4 异常处理 清洗细节 • 日期格式统一为 datetime • 列名重命名为英文 • 删除净值 ≤ 0 的行 • 按日期升序排列 对齐细节 • 提取每只基金净值列 • 合并为宽表格式 • 前向填充缺失值 • 或只保留共同日期 异常处理细节 • IQR 方法检测离群点 • Z-score 方法检测 • 前向填充替换异常值 • 或直接删除异常行 输出:干净、对齐的净值数据

6. 实战中的几个坑

最后分享几个我踩过的坑:

  • 基金代码搞混:ETF基金和场外基金代码不同,别混用。我一开始用510300的场外代码去拉数据,结果啥也没拉到。
  • 复权方式选错:回测用前复权,实盘用后复权。搞反了收益率会算错。
  • 节假日处理:A股有春节、国庆长假,数据会断档。对齐时别用连续日期,要用交易日历。
  • 数据更新延迟:akshare的数据一般T+1更新,当天收盘后要等第二天才能拿到完整数据。
核心原则: 数据预处理的目标不是让数据变得完美,而是让数据变得可用。接受一定程度的不完美,但要确保你的策略对数据质量有鲁棒性。

好了,数据获取和预处理就聊到这儿。下一节我们会用这些干净的数据,开始做真正的调仓策略。记住:数据质量决定了策略的上限,策略本身只是把数据价值变现的工具。

专注资料整理