4. 数据获取与清洗:从 Tushare/akshare 获取行情数据、数据缺失值处理、复权价格计算
量化回测的基石是高质量、连续、无偏的行情数据。本章聚焦于从国内主流数据源(Tushare Pro 与 akshare)获取原始数据,并完成清洗与复权处理,确保回测引擎输入的数据口径一致、逻辑正确。
4.1 数据源选型对比
| 特性 | Tushare Pro | akshare |
|---|---|---|
| 接口类型 | RESTful API(需 token) | 直接 HTTP 请求 + 解析 |
| 数据质量 | 高(交易所授权合作) | 中高(聚合多源) |
| 复权支持 | 原生参数(qfq/hfq) | 需手动计算或调用复权因子 |
| 频率覆盖 | 日/周/月/分钟 | 日/分钟/实时 tick |
| 免费额度 | 积分制(基础 200 分) | 无限制(开源) |
| 适用场景 | 专业回测、因子研究 | 快速原型、实时数据 |
选型建议: 生产级回测优先使用 Tushare Pro 的复权接口;研究阶段或需要高频 tick 数据时,akshare 的免费特性更具优势。两者可互补使用。
4.2 数据获取流程
以下代码演示从 Tushare Pro 获取沪深 300 成分股日线数据,并同时展示 akshare 的等效调用方式。
4.2.1 Tushare Pro 获取日线
import tushare as ts
import pandas as pd
# 初始化(请替换为你的 token)
pro = ts.pro_api('your_tushare_token')
# 获取平安银行(000001.SZ)日线,前复权
df_ts = pro.daily(
ts_code='000001.SZ',
start_date='20230101',
end_date='20231231',
adj='qfq' # 前复权
)
# 字段重命名与排序
df_ts.rename(columns={
'trade_date': 'date',
'vol': 'volume'
}, inplace=True)
df_ts['date'] = pd.to_datetime(df_ts['date'])
df_ts.sort_values('date', inplace=True)
df_ts.reset_index(drop=True, inplace=True)
print(df_ts.head())
4.2.2 akshare 获取日线
import akshare as ak
# 获取平安银行日线(未复权)
df_ak = ak.stock_zh_a_hist(
symbol="000001",
period="daily",
start_date="20230101",
end_date="20231231",
adjust="" # 空字符串表示未复权
)
# 字段标准化
df_ak.rename(columns={
'日期': 'date',
'开盘': 'open',
'收盘': 'close',
'最高': 'high',
'最低': 'low',
'成交量': 'volume',
'成交额': 'amount'
}, inplace=True)
df_ak['date'] = pd.to_datetime(df_ak['date'])
df_ak.sort_values('date', inplace=True)
df_ak.reset_index(drop=True, inplace=True)
print(df_ak.head())
注意: akshare 返回的字段名为中文,需手动映射为英文标准字段。Tushare Pro 返回的字段名已是英文,但需注意
vol 代表成交量(手),amount 代表成交额(元)。
4.3 数据缺失值处理
行情数据常见的缺失场景包括:停牌日、新股上市前、退市后、节假日非交易日。处理策略需根据回测逻辑选择。
| 缺失类型 | 处理方法 | 适用场景 |
|---|---|---|
| 停牌日(无交易) | 前向填充(ffill) | 持仓市值计算、因子值延续 |
| 新股上市前 | 删除或填充 NaN | 回测起始日之后才纳入样本 |
| 退市后 | 截断至最后交易日 | 避免未来信息泄露 |
| 非交易日(周末/节假日) | 不填充,保持自然缺失 | 仅交易日对齐 |
# 示例:停牌日前向填充
df = df.set_index('date')
# 生成完整交易日日历
all_dates = pd.date_range(start='2023-01-01', end='2023-12-31', freq='B')
df = df.reindex(all_dates)
# 前向填充停牌日数据
df[['open', 'high', 'low', 'close', 'volume']] = df[[
'open', 'high', 'low', 'close', 'volume'
]].fillna(method='ffill')
# 成交量缺失(停牌)填充为 0
df['volume'] = df['volume'].fillna(0)
# 删除仍为 NaN 的行(如上市前)
df.dropna(subset=['close'], inplace=True)
关键原则: 回测中绝不允许使用未来数据填充缺失值(如后向填充)。前向填充是唯一符合信息流的处理方式。
4.4 复权价格计算
复权是为了消除分红、送股、配股等事件对价格序列造成的非交易性跳变。回测必须使用复权价格,否则收益率计算会失真。
4.4.1 复权类型对比
| 类型 | 计算方式 | 回测适用性 |
|---|---|---|
| 前复权(qfq) | 调整历史价格,使当前价格不变 | 最常用,收益率序列连续 |
| 后复权(hfq) | 调整当前价格,使历史价格不变 | 适合长期走势分析 |
| 不复权 | 原始价格,存在跳空缺口 | 仅用于除权除息日分析 |
4.4.2 手动复权计算(以 akshare 为例)
当数据源未提供复权数据时,可通过复权因子自行计算。
# 获取复权因子(akshare)
factor_df = ak.stock_zh_a_adjust(symbol="000001", start_date="20230101", end_date="20231231")
# 假设 factor_df 包含 'date' 和 'adj_factor' 列
# 合并到行情数据
df = df.merge(factor_df[['date', 'adj_factor']], on='date', how='left')
# 前复权计算:最新复权因子为基准
latest_factor = df['adj_factor'].iloc[-1]
df['close_adj'] = df['close'] * df['adj_factor'] / latest_factor
df['open_adj'] = df['open'] * df['adj_factor'] / latest_factor
df['high_adj'] = df['high'] * df['adj_factor'] / latest_factor
df['low_adj'] = df['low'] * df['adj_factor'] / latest_factor
# 成交量不需要复权
df['volume_adj'] = df['volume']
验证方法: 复权后的价格序列应单调连续,除权除息日前后价格不会出现跳空。可绘制复权前后对比图进行目视检查。
4.5 数据清洗与复权流程总览
下图展示了从原始数据到回测可用数据的完整处理链路。
4.6 数据口径校验清单
在将数据送入回测引擎前,建议逐项检查以下口径。
- 时间对齐: 所有股票使用同一交易日历,避免因停牌导致长度不一致。
- 字段统一: 强制使用
date、open、high、low、close、volume、amount标准命名。 - 复权标记: 在 DataFrame 的
attrs或列注释中记录复权类型(qfq/hfq/none)。 - 极端值过滤: 检查涨跌幅是否超过交易所限制(A 股主板 ±10%,科创/创业板 ±20%)。
- 成交量单位: 确认是“股”还是“手”,回测中统一转换为“股”。
最佳实践: 将数据获取、清洗、复权封装为独立的
DataLoader 类,支持多数据源切换,并输出统一的 OHLCV 格式。这样回测引擎无需关心底层数据来源,只需消费标准接口。