第四章:数据获取与处理

做量化回测,数据就是你的弹药库。

弹药不行,再好的策略也白搭。我见过太多人,策略逻辑写得漂漂亮亮,结果数据一塌糊涂,回测结果根本没法看。今天咱们就来聊聊,怎么把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 本章知识体系

下面这张图,是我梳理的数据处理全流程:

数据获取与处理流程 步骤1:数据获取 Tushare pro.daily() 步骤2:数据清洗 缺失值处理 复权处理 步骤3:特征计算 收益率计算 技术指标 步骤4:选择存储格式 CSV格式 HDF5格式 回测系统使用

这张图把整个流程串起来了。从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