第四章:数据获取与清洗实战
各位同学,欢迎来到实战环节。前面几章我们聊了很多理论,什么因子逻辑、组合优化,听着都挺美。但说句实在话,我做了这么多年量化,最大的感触就是:数据不行,模型白搭。你策略再牛,回测曲线再漂亮,数据一脏,全得重来。
这一章,我们就来啃这块硬骨头。我会带着大家,用 Tushare 把期货日线数据拿到手,然后一步步清洗干净,最后算好复权因子,把价格复权。嗯,这套流程我几乎每个项目都要走一遍,闭着眼睛都能写出来。
核心逻辑: 数据获取 → 缺失值处理 → 异常值检测 → 复权计算 → 价格复权。每一步都踩过坑,咱们一个一个来填。
4.1 用 Tushare 拉数据,其实没那么玄乎
Tushare 这个库,国内做量化的应该都用过。我个人习惯用 pro 版,数据全,接口稳定。你想想看,要是每天手动去交易所网站扒数据,那得多累?
先装好库:
pip install tushare
然后注册个账号,拿到 token。这一步别偷懒,我见过有人直接用网上公开的 token,结果用两天就被封了。
4.1.1 获取期货日线数据
期货数据和股票不太一样。股票你直接拿复权价就行,但期货有换月、有交割,数据更复杂。我们一般拿主力连续合约,或者自己拼接。
import tushare as ts
import pandas as pd
# 初始化
pro = ts.pro_api('你的token')
# 获取螺纹钢主力连续合约日线
df = pro.fut_daily(
ts_code='RB9999.XSGE', # 9999代表主力连续
start_date='20200101',
end_date='20231231',
fields='trade_date,open,high,low,close,vol,amount,hold'
)
# 看一眼数据
print(df.head())
跑完你会发现,数据量挺大,但里面有不少坑。比如某些日期停牌,数据直接是空的。还有的时候,成交量突然变成 0,这明显有问题。
小技巧: 我一般会多拉几个品种,比如螺纹钢、铁矿石、甲醇,放在一个字典里。这样后面做多因子分析时,数据都在手边。
4.2 缺失值处理——别让空值毁了你的模型
数据拿到手,第一件事就是检查缺失值。你想想看,如果某个因子在某天是 NaN,模型算出来就是 NaN,整个信号就断了。
我习惯先看看缺失比例:
# 检查缺失值
missing_ratio = df.isnull().sum() / len(df)
print(missing_ratio)
# 如果缺失比例小于5%,直接填充
df['vol'].fillna(method='ffill', inplace=True) # 用前一天数据填充
df['open'].fillna(method='bfill', inplace=True) # 用后一天数据填充
这里要注意,千万别用均值填充。期货数据有趋势,用均值会把趋势抹掉。我一般用前向填充(ffill),因为价格变化是连续的,前一天的数据最接近真实值。
避坑指南: 我曾经遇到过一个品种,连续三天没有交易。用 ffill 填充后,模型以为价格没变,结果第四天开盘跳空,策略直接爆亏。所以,如果连续缺失超过 3 天,我建议直接删除这些日期。
4.3 异常值检测——把那些离谱的数据揪出来
异常值这东西,说白了就是数据里的「坏蛋」。比如某天收盘价突然涨了 20%,但成交量只有平时的十分之一。这明显是数据录入错误,或者是极端行情下的异常交易。
我常用的方法是 3σ 原则 和 IQR 方法。这里我推荐 IQR,因为它不受极端值影响,更稳健。
# 用 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
# 找出异常值
outliers = df[(df['close'] < lower_bound) | (df['close'] > upper_bound)]
print(f"发现 {len(outliers)} 个异常值")
# 修正:用中位数替换
median_val = df['close'].median()
df.loc[outliers.index, 'close'] = median_val
嗯,这里要注意,不要直接删除异常值。你删掉一行,时间序列就断了,后面算收益率会出错。我一般用中位数或者前后两天的均值来替换。
个人经验: 有一次我检测到铁矿石的持仓量突然翻了 10 倍,但价格没怎么动。后来发现是交易所调整了保证金比例,导致持仓数据异常。这种情况下,我会保留数据,但会在因子计算时做特殊处理。
4.4 复权因子计算——期货的复权和股票不一样
股票的复权,大家应该都懂。但期货的复权,很多人会搞混。期货没有分红送股,但存在换月价差。比如螺纹钢 1 月合约和 5 月合约,价格可能差 100 点。如果你直接拼接,回测结果会失真。
我一般用 前复权 方法,把历史价格调整到当前合约的水平。具体做法是:
- 找到换月日期(比如主力合约切换日)
- 计算换月前后的价差
- 用这个价差调整历史价格
# 假设我们有两个合约的数据
contract1 = df[df['ts_code'] == 'RB2401.SHFE']
contract2 = df[df['ts_code'] == 'RB2405.SHFE']
# 找到换月日期(这里假设是2024-01-15)
roll_date = '2024-01-15'
price_diff = contract2.loc[roll_date, 'close'] - contract1.loc[roll_date, 'close']
# 前复权:调整历史价格
contract1['adj_close'] = contract1['close'] + price_diff
contract2['adj_close'] = contract2['close']
小技巧: 我一般会计算一个「复权因子」序列,而不是直接改价格。这样以后想切换复权方式,直接改因子就行,不用重新算。
4.5 价格复权——把数据对齐到同一基准
最后一步,就是把所有价格都复权到同一个基准上。这样,你算出来的收益率、波动率等因子,才是真实可比的。
# 计算复权因子
df['adj_factor'] = 1.0 # 初始化
# 假设我们有一个换月价差序列
for i in range(1, len(df)):
if df.loc[df.index[i], 'ts_code'] != df.loc[df.index[i-1], 'ts_code']:
# 换月了,调整因子
price_old = df.loc[df.index[i-1], 'close']
price_new = df.loc[df.index[i], 'close']
df.loc[df.index[i], 'adj_factor'] = price_new / price_old
else:
df.loc[df.index[i], 'adj_factor'] = df.loc[df.index[i-1], 'adj_factor']
# 应用复权因子
df['adj_open'] = df['open'] * df['adj_factor']
df['adj_close'] = df['close'] * df['adj_factor']
df['adj_high'] = df['high'] * df['adj_factor']
df['adj_low'] = df['low'] * df['adj_factor']
你看,这样算出来的复权价格,才是真正可以用来做回测的。我见过有人直接用原始价格跑回测,结果换月那天收益率突然跳变,还以为自己发现了 Alpha,其实是数据没处理干净。
总结一下: 数据清洗这件事,看似枯燥,但决定了你整个量化体系的成败。我做了这么多年,每次拿到新数据,还是会老老实实走一遍这个流程。别嫌麻烦,磨刀不误砍柴工。
好了,这一章的内容就到这里。数据拿到手、清洗干净、复权完成,下一步我们就可以开始构建因子了。嗯,那咱们下一章见。