4. 数据获取与清洗:使用Tushare/AkShare获取行情数据、数据清洗与对齐、处理缺失值与异常值

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

我见过太多人,策略逻辑写得漂漂亮亮,一跑回测就崩。为什么?数据没处理好。今天咱们就把这块硬骨头啃下来。

4.1 数据源的选择:Tushare vs AkShare

国内做量化,绕不开两个库:Tushare 和 AkShare。我个人的习惯是,两个都装,看场景切换。

对比维度 Tushare Pro AkShare
数据质量 高,经过校验 中等,部分来自网络爬虫
获取方式 需要 token,有积分限制 免费,无需注册
速度 快,有缓存机制 较慢,依赖源网站
覆盖范围 A股、期货、基金、指数 A股、期货、外汇、宏观经济
适合场景 正式回测、实盘 快速验证、研究
我的建议:初期用 AkShare 做策略验证,省事。等策略定型了,再切到 Tushare 做正式回测。我在项目中遇到过,用 AkShare 的数据跑回测收益不错,换到 Tushare 数据后收益直接腰斩——不是策略问题,是数据精度差异。

4.2 实战:用 Tushare 获取日线数据

先装库:

pip install tushare akshare pandas numpy

然后注册 Tushare,拿到你的 token。嗯,这一步别偷懒。

import tushare as ts
import pandas as pd

# 设置 token
ts.set_token('你的token_here')
pro = ts.pro_api()

# 获取平安银行 2023年日线数据
df = pro.daily(
    ts_code='000001.SZ',
    start_date='20230101',
    end_date='20231231'
)

# 按日期排序
df = df.sort_values('trade_date')
print(df.head())

返回的数据长这样:

   ts_code trade_date  open  high   low  close  pre_close  change  pct_chg  vol        amount
0  000001.SZ   20230103  12.5  12.8  12.3   12.6       12.4    0.20    1.61  123456  1.56e+09
1  000001.SZ   20230104  12.7  13.0  12.5   12.9       12.6    0.30    2.38  134567  1.72e+09
注意:Tushare 的 daily 接口默认返回最近的数据,日期是倒序的。一定要做 sort_values,否则后面计算技术指标时会出大问题。我曾经因为这个 bug 排查了整整一个下午。

4.3 用 AkShare 获取数据

AkShare 更直接,不用 token:

import akshare as ak

# 获取上证指数日线
df_index = ak.stock_zh_index_daily(symbol="sh000001")
print(df_index.head())

不过要注意,AkShare 的字段名和 Tushare 不太一样。比如日期字段,Tushare 叫 trade_date,AkShare 可能叫 date。做多数据源时,记得统一字段名。

4.4 数据清洗:脏数据怎么处理?

数据拿到手,别急着用。先看看有没有坑。

4.4.1 检查缺失值

# 检查缺失值
print(df.isnull().sum())

# 看看哪些行有问题
print(df[df.isnull().any(axis=1)])

缺失值怎么处理?分情况:

  • 少量缺失(<5%):用前向填充 ffill() 或插值 interpolate()
  • 连续缺失:用前后均值填充
  • 关键字段缺失:直接删除该行
# 前向填充
df['close'] = df['close'].ffill()

# 线性插值
df['close'] = df['close'].interpolate(method='linear')

# 删除缺失行
df = df.dropna(subset=['close', 'open', 'high', 'low'])
避坑指南:我曾经用 ffill() 填充了连续一周的缺失数据,结果回测时发现那周策略一直在"假交易"。因为填充的数据是重复的,导致信号重复触发。后来我改成:连续缺失超过3天,直接删除这段数据。

4.4.2 处理异常值

异常值比缺失值更隐蔽。比如某天收盘价突然涨了50%,但大盘没动——这大概率是数据错误。

# 用 Z-score 检测异常值
from scipy import stats

z_scores = stats.zscore(df['pct_chg'])
abs_z_scores = np.abs(z_scores)
filtered_entries = (abs_z_scores < 3)  # 保留3个标准差以内的数据
df_clean = df[filtered_entries]

或者用 IQR 方法:

Q1 = df['close'].quantile(0.25)
Q3 = df['close'].quantile(0.75)
IQR = Q3 - Q1

# 定义异常值范围
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 过滤
df_clean = df[(df['close'] >= lower_bound) & (df['close'] <= upper_bound)]
注意:别一刀切。比如股票除权除息时,价格会突然跳变,这不是异常值。我建议先做复权处理,再检测异常。

4.5 数据对齐:多品种时间序列的痛点

做多品种策略时,你会发现:A股票交易日和B股票交易日不完全一样。比如A在1月1日交易了,B那天休市。怎么对齐?

# 假设有两个股票的数据
df_a = pd.DataFrame({
    'date': ['2023-01-01', '2023-01-02', '2023-01-03'],
    'close': [10, 11, 12]
})
df_b = pd.DataFrame({
    'date': ['2023-01-01', '2023-01-03', '2023-01-04'],
    'close': [20, 22, 23]
})

# 合并对齐
df_merged = pd.merge(df_a, df_b, on='date', how='outer', suffixes=('_a', '_b'))
df_merged = df_merged.sort_values('date')

# 前向填充缺失值
df_merged = df_merged.ffill()

这里有个细节:用 how='outer' 保留所有交易日,然后用 ffill() 填充。为什么不用 inner?因为如果只用交集,会丢失很多数据点,影响策略的连续性。

4.6 核心逻辑:数据清洗流程图

下面这张图,是我做数据清洗的标准流程。你照着走,基本不会出大问题。

数据清洗与对齐流程图 原始数据获取 缺失值检查 异常值检测 多品种数据对齐 清洗后数据输出 关键说明 • 缺失值 < 5%:ffill/插值 • 缺失值 > 5%:删除该段 • 异常值:Z-score / IQR • 复权处理:前复权优先 • 对齐方式:outer + ffill • 日期格式统一为 YYYYMMDD ⚠ 常见坑点 1. 除权除息未处理 2. 停牌日数据缺失 3. 不同源数据字段名不一致 4. 时间戳时区问题

4.7 完整的数据清洗函数

最后,给你一个可以直接用的函数。我项目里一直在用,经过多次迭代,比较稳定。

def clean_market_data(df, date_col='trade_date', price_cols=['open', 'high', 'low', 'close']):
    """
    清洗行情数据
    """
    # 1. 排序
    df = df.sort_values(date_col).reset_index(drop=True)
    
    # 2. 检查缺失
    missing_ratio = df[price_cols].isnull().sum() / len(df)
    if missing_ratio.max() > 0.05:
        print(f"警告:缺失率超过5%,建议检查数据源")
    
    # 3. 填充缺失
    df[price_cols] = df[price_cols].fillna(method='ffill', limit=3)
    df[price_cols] = df[price_cols].fillna(method='bfill', limit=3)
    
    # 4. 检测异常(涨跌幅超过20%视为异常)
    df['pct_chg'] = df['close'].pct_change()
    abnormal = df[abs(df['pct_chg']) > 0.2].index
    if len(abnormal) > 0:
        print(f"发现 {len(abnormal)} 个异常值,已标记")
        df.loc[abnormal, price_cols] = None
        df[price_cols] = df[price_cols].interpolate()
    
    # 5. 删除剩余缺失
    df = df.dropna(subset=price_cols)
    
    return df
核心要点:数据清洗不是一次性的工作。每次新增数据源、调整策略周期,都要重新跑一遍清洗流程。我建议把清洗逻辑封装成函数,每次调用即可。

好了,数据获取和清洗这块就讲到这里。你想想看,如果数据本身就有问题,后面再好的策略也是白搭。所以,花时间把数据搞干净,绝对值得。

专注资料整理