第二章:数据获取与清洗——用tushare/akshare搞定A股日线数据

做量化交易,数据就是你的弹药。弹药不行,枪法再好也白搭。

这一章,咱们就聊聊怎么拿到干净、可用的A股日线数据。我会用tushare和akshare这两个库,手把手带你走一遍。我个人习惯用akshare多一些,因为它免费且数据源稳定。但tushare在某些场景下也有优势,比如复权因子的处理更直接。

2.1 数据获取:从零到一

先说说数据源的选择。你想想看,市面上能免费拿到A股数据的渠道其实不多。tushare是老牌选手,akshare是后起之秀。我的建议是:

  • tushare:适合需要高频数据、复权因子、财务数据的场景。但需要积分,新手可能有点门槛。
  • akshare:完全免费,接口丰富,覆盖股票、基金、期货、宏观数据。我最近的项目基本都用它。

嗯,这里要注意:akshare的数据源来自东方财富、新浪财经等,偶尔会有延迟或缺失。但做日线级别的分析,完全够用。

2.1.1 安装与初始化

# 安装
pip install akshare tushare

# 初始化
import akshare as ak
import tushare as ts
import pandas as pd
import numpy as np

# tushare需要token
ts.set_token('你的token')
pro = ts.pro_api()

我在项目中遇到过一个问题:tushare的token如果过期了,会直接报错。所以建议把token存在环境变量里,别硬编码在代码中。

2.1.2 获取日线数据

以贵州茅台(600519)为例,看看两个库的用法差异。

# akshare方式
df_ak = ak.stock_zh_a_hist(symbol="600519", period="daily", 
                           start_date="20230101", end_date="20231231", 
                           adjust="qfq")  # qfq=前复权

# tushare方式
df_ts = pro.daily(ts_code="600519.SH", start_date="20230101", 
                  end_date="20231231")

你看,akshare直接传参adjust="qfq"就能拿到前复权数据。tushare返回的是未复权数据,需要额外处理复权因子。这一点,akshare更省事。

我的小技巧:如果你要批量获取多只股票,建议用akshare的循环请求,每次间隔0.5秒,避免被反爬。我曾经一次性请求500只股票,结果IP被封了半小时...

2.2 复权因子处理:别让分红影响你的判断

为什么要处理复权?说白了,股票分红、送股会导致价格出现跳空缺口。如果你直接用原始价格计算收益率,结果会失真。

举个例子:某股票100元,10送10后变成50元。如果你用50元算历史收益率,会以为跌了50%。但实际上你的资产没变。复权就是把这个缺口补上。

2.2.1 前复权 vs 后复权

类型 含义 适用场景
前复权 调整历史价格,使当前价格不变 技术分析、回测(最常用)
后复权 调整当前价格,使历史价格不变 看真实涨幅、长期投资分析
不复权 原始价格,有缺口 分红套利、事件驱动策略

我个人习惯用前复权做回测。为什么呢?因为前复权保证了当前价格是真实的市场价,回测结果更贴近实际交易。

2.2.2 手动处理复权因子

如果你用tushare,需要自己算复权价格。代码其实不复杂:

# 获取复权因子
adj_factor = pro.adj_factor(ts_code="600519.SH", 
                            start_date="20230101", 
                            end_date="20231231")

# 合并到日线数据
df = df_ts.merge(adj_factor[['trade_date', 'adj_factor']], 
                 on='trade_date', how='left')

# 计算前复权价格
df['adj_close'] = df['close'] * df['adj_factor'] / df['adj_factor'].iloc[-1]
df['adj_open'] = df['open'] * df['adj_factor'] / df['adj_factor'].iloc[-1]
避坑指南:我曾经在回测中直接用了未复权数据,结果策略表现异常好。后来发现是因为分红导致的假收益。嗯,从那以后,我每次回测前都会检查数据是否复权。

2.3 清洗异常值:数据质量决定策略上限

数据拿到手,别急着用。先看看有没有脏数据。我总结了一套清洗流程,分享给你:

2.3.1 常见异常类型

  • 缺失值:停牌、节假日导致的数据空缺
  • 极端值:涨跌停、数据录入错误(比如价格突然变成0或99999)
  • 重复值:同一日期出现多条记录
  • 逻辑错误:开盘价=收盘价、最高价<最低价等

2.3.2 清洗实战代码

def clean_stock_data(df):
    """
    清洗股票日线数据
    """
    # 1. 删除重复行
    df = df.drop_duplicates(subset=['date'])
    
    # 2. 处理缺失值
    # 停牌日期的价格用前一个交易日填充
    df = df.fillna(method='ffill')
    
    # 3. 检查逻辑错误
    # 最高价不能小于最低价
    invalid = df[df['high'] < df['low']]
    if len(invalid) > 0:
        print(f"发现{len(invalid)}条逻辑错误数据,已修正")
        # 交换最高价和最低价
        df.loc[df['high'] < df['low'], ['high', 'low']] = \
            df.loc[df['high'] < df['low'], ['low', 'high']].values
    
    # 4. 处理极端值
    # 涨跌幅超过20%的视为异常(A股普通股票涨跌幅限制10%)
    df['pct_chg'] = df['close'].pct_change()
    extreme = df[abs(df['pct_chg']) > 0.2]
    if len(extreme) > 0:
        print(f"发现{len(extreme)}条极端涨跌幅数据")
        # 用前后均值替换
        for idx in extreme.index:
            df.loc[idx, 'close'] = (df.loc[idx-1, 'close'] + 
                                    df.loc[idx+1, 'close']) / 2
    
    return df

你可能会问:为什么涨跌幅超过20%就算异常?因为A股主板涨跌幅限制是10%,科创板是20%。如果你做的是科创板股票,阈值要调到40%。这个细节,我在项目中吃过亏。

2.3.3 数据质量检查清单

每次清洗完数据,我都会跑一遍这个检查:

  1. 数据量是否完整?比如2023年应该有245个交易日左右
  2. 是否有空值?用df.isnull().sum()检查
  3. 价格是否为正数?用df.describe()看最小值
  4. 日期是否连续?用df['date'].diff().value_counts()检查间隔
核心原则:宁可少数据,不要脏数据。一条异常值可能让你的策略回测结果偏差10%以上。

2.4 本章知识体系

下面这张图,帮你梳理了数据获取与清洗的完整流程:

数据获取与清洗流程 数据源选择 获取方式:akshare / tushare 复权处理:前复权 / 后复权 数据清洗:缺失值 / 极端值 / 逻辑错误 干净可用的日线数据 第1步 第2步 第3步 第4步 第5步

这张图把整个流程串起来了。从数据源选择,到获取方式,再到复权处理和数据清洗,最后输出干净数据。每一步都环环相扣。

我的经验:数据清洗这一步,我通常会写成一个独立的函数,放在项目的utils模块里。每次新项目直接调用,省时省力。你想想看,如果每次都要重写一遍,多浪费时间。

好了,数据获取和清洗就聊到这里。记住一句话:数据质量决定策略上限。花80%的时间在数据上,剩下20%做策略,这个比例是值得的。

专注资料整理