第四章:数据获取与处理
做量化回测,数据就是你的弹药库。
弹药不行,再好的策略也白搭。我见过太多人,策略逻辑写得漂漂亮亮,结果数据一塌糊涂,回测结果根本没法看。今天咱们就来聊聊,怎么把Tushare的数据变成能用的回测数据。
4.1 为什么数据预处理这么重要?
你想想看,原始数据就像刚从菜市场买回来的菜——带着泥、带着根、还有烂叶子。直接下锅?那味道肯定不对。
我刚开始做回测时,就吃过这个亏。拿到的日线数据里有停牌日、有除权除息缺口,我直接拿来算收益率,结果策略表现好得离谱。后来才发现,那是数据没处理干净导致的假象。
核心原则:数据预处理不是可选项,是必选项。它直接决定了回测结果的可靠性。
4.2 使用Tushare获取股票数据
Tushare是目前国内用得比较多的免费数据源。我个人习惯用pro版,数据质量相对稳定。
先安装并导入:
import tushare as ts
import pandas as pd
import numpy as np
# 设置token,建议从环境变量读取
ts.set_token('你的token')
pro = ts.pro_api()
获取日线数据:
# 获取平安银行2023年日线数据
df = pro.daily(
ts_code='000001.SZ',
start_date='20230101',
end_date='20231231'
)
# 按日期排序
df = df.sort_values('trade_date').reset_index(drop=True)
print(df.head())
| 字段名 | 含义 | 注意事项 |
|---|---|---|
| trade_date | 交易日期 | 格式为YYYYMMDD,需转为datetime |
| open/high/low/close | 开盘/最高/最低/收盘价 | 复权前数据,需自行处理 |
| vol | 成交量(手) | 注意单位,1手=100股 |
| amount | 成交额(千元) | 需转换为元 |
我的小习惯:每次获取数据后,先检查一下数据量。比如一年应该有240-250个交易日,如果少了很多,可能是停牌或者数据缺失。
4.3 数据清洗与预处理
数据拿到手,接下来就是重头戏了。我总结了一套「三步走」流程,你可以参考一下。
4.3.1 第一步:处理缺失值
为什么会缺失?停牌、节假日、数据源问题,都有可能。
# 检查缺失值
print(df.isnull().sum())
# 处理停牌日:用前一个交易日填充
df['close'] = df['close'].fillna(method='ffill')
df['open'] = df['open'].fillna(method='ffill')
# 如果开盘价也缺失,说明全天无交易
# 我建议直接删除这些行
df = df.dropna(subset=['open', 'close'])
注意:千万别用均值填充股票价格!我曾经见过有人这么干,结果回测出来的策略在停牌日「精准」抄底,实际根本不可能成交。
4.3.2 第二步:复权处理
股票会分红、送股、配股。这些事件会导致价格出现跳空缺口。如果不处理,你的回测会严重失真。
Tushare提供了复权因子数据:
# 获取复权因子
adj_factor = pro.adj_factor(ts_code='000001.SZ',
start_date='20230101',
end_date='20231231')
# 合并到日线数据
df = df.merge(adj_factor[['trade_date', 'adj_factor']],
on='trade_date', how='left')
# 计算后复权价格
df['close_adj'] = df['close'] * df['adj_factor']
df['open_adj'] = df['open'] * df['adj_factor']
我个人习惯用后复权数据做回测。为什么?因为后复权价格能真实反映持有期间的收益,包括分红再投资的效果。
4.3.3 第三步:计算收益率
收益率是回测中最基础的指标:
# 简单收益率
df['return'] = df['close_adj'].pct_change()
# 对数收益率(我更喜欢这个,因为它可加性更好)
df['log_return'] = np.log(df['close_adj'] / df['close_adj'].shift(1))
# 删除第一行的NaN
df = df.dropna(subset=['return'])
避坑指南:计算收益率时,一定要用复权后的价格。我曾经用未复权数据算收益率,结果策略年化收益算出来30%,实际只有10%。那差距,啧啧。
4.4 数据存储:CSV vs HDF5
数据清洗完了,得存起来。我一般两种格式都用,看场景。
4.4.1 CSV格式
简单、通用、人类可读。适合小规模数据(几千只股票以内)。
# 保存为CSV
df.to_csv('000001_SZ_2023.csv', index=False, encoding='utf-8-sig')
# 读取CSV
df = pd.read_csv('000001_SZ_2023.csv', parse_dates=['trade_date'])
4.4.2 HDF5格式
速度快、压缩率高、支持复杂查询。适合大规模数据。
# 保存为HDF5
df.to_hdf('stock_data.h5', key='000001_SZ', mode='a',
format='table', data_columns=True)
# 读取HDF5
df = pd.read_hdf('stock_data.h5', key='000001_SZ')
# 支持条件查询
df = pd.read_hdf('stock_data.h5', key='000001_SZ',
where='trade_date > "20230601"')
| 对比项 | CSV | HDF5 |
|---|---|---|
| 读写速度 | 慢(尤其大文件) | 快(二进制格式) |
| 存储空间 | 大(文本格式) | 小(支持压缩) |
| 可读性 | 好(记事本可打开) | 差(需专用工具) |
| 跨平台 | 极好 | 好(需安装h5py) |
| 适合场景 | 小数据、调试、分享 | 大数据、频繁读写 |
我的建议:开发阶段用CSV,方便查看数据。正式回测用HDF5,速度快很多。我自己的回测系统,每天增量更新数据到HDF5,几万只股票的数据,读写都在毫秒级。
4.5 本章知识体系
下面这张图,是我梳理的数据处理全流程:
这张图把整个流程串起来了。从Tushare拿数据,到清洗、计算特征,最后存成CSV或HDF5。每一步都有坑,但走通了,后面的回测就稳了。
4.6 完整代码示例
最后,给你一个可以直接跑的完整脚本:
import tushare as ts
import pandas as pd
import numpy as np
def fetch_and_clean_data(ts_code, start_date, end_date):
"""
获取并清洗股票数据
"""
# 1. 获取数据
pro = ts.pro_api()
df = pro.daily(ts_code=ts_code,
start_date=start_date,
end_date=end_date)
df = df.sort_values('trade_date').reset_index(drop=True)
# 2. 处理缺失值
df['close'] = df['close'].fillna(method='ffill')
df = df.dropna(subset=['open', 'close'])
# 3. 复权处理
adj = pro.adj_factor(ts_code=ts_code,
start_date=start_date,
end_date=end_date)
df = df.merge(adj[['trade_date', 'adj_factor']],
on='trade_date', how='left')
df['close_adj'] = df['close'] * df['adj_factor']
# 4. 计算收益率
df['return'] = df['close_adj'].pct_change()
df = df.dropna(subset=['return'])
return df
# 使用示例
data = fetch_and_clean_data('000001.SZ', '20230101', '20231231')
# 存储
data.to_hdf('stock_data.h5', key='000001_SZ', mode='w')
print(f"数据已保存,共{len(data)}条记录")
最后说一句:数据预处理这件事,看起来琐碎,但值得花时间。我见过太多人,策略写得天花乱坠,结果数据一查全是错的。记住一句话:垃圾进,垃圾出。数据质量决定了回测的底线。
公众号:蓝海数据掘金营,微信deep3321