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 核心逻辑:数据清洗流程图
下面这张图,是我做数据清洗的标准流程。你照着走,基本不会出大问题。
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
核心要点:数据清洗不是一次性的工作。每次新增数据源、调整策略周期,都要重新跑一遍清洗流程。我建议把清洗逻辑封装成函数,每次调用即可。
好了,数据获取和清洗这块就讲到这里。你想想看,如果数据本身就有问题,后面再好的策略也是白搭。所以,花时间把数据搞干净,绝对值得。