第三章:数据获取与处理——量化交易的“原材料”工程
做量化交易,尤其是商品CTA,最怕什么?
不是策略亏钱,而是数据出错。
我见过太多人,策略逻辑写得天花乱坠,结果回测曲线漂亮得像假的一样——一查,数据没复权,或者主力合约换月没处理。嗯,这种坑,我早年也踩过。
这一章,咱们就把数据这块硬骨头啃下来。说白了,数据搞不定,后面全是白搭。
3.1 获取期货行情数据:从哪里来?
期货数据源,市面上就那么几个靠谱的。我个人习惯用以下三种:
- Tushare Pro:国内老牌,接口稳定,但需要积分。适合个人研究。
- Wind / 聚宽:机构常用,数据质量高,但贵。适合团队。
- 交易所官网:免费,但需要自己爬。适合练手。
我建议初学者先用Tushare。为什么?因为省心。你想想看,如果数据获取就花掉你80%的时间,哪还有精力搞策略?
核心原则:数据源要稳定、字段要全、频率要够。对于CTA策略,至少需要:开盘价、最高价、最低价、收盘价、成交量、持仓量。
代码示例(Tushare获取主力连续合约):
import tushare as ts
# 初始化(记得换成你自己的token)
pro = ts.pro_api('your_token_here')
# 获取螺纹钢主力连续合约日线数据
df = pro.fut_daily(
ts_code='RB.SHF', # 上海期货交易所螺纹钢
start_date='20200101',
end_date='20231231',
fields='trade_date,open,high,low,close,vol,hold'
)
print(df.head())
这里有个坑:ts_code的格式是固定的,比如螺纹钢是RB.SHF,铁矿石是I.DCE。我曾经因为写错了交易所后缀,白跑了一周的数据……
3.2 数据清洗与对齐:脏数据比没数据更可怕
拿到原始数据后,别急着用。先做三件事:
- 去重:同一个交易日出现两条记录?保留第一条,删掉重复的。
- 补缺失:遇到停牌或节假日,用前值填充(ffill),或者直接删除。
- 对齐时间戳:不同合约的交易时间可能不同,必须统一到同一个时间轴上。
警告:千万不要用dropna()无脑删除缺失值!我曾经在回测中删掉了某天的数据,结果策略在那天本该开仓,却因为数据缺失跳过了——回测曲线瞬间变漂亮,实盘却亏成狗。
对齐的代码示例:
import pandas as pd
# 假设你有两个合约的数据:rb_main(主力)和 rb_second(次主力)
# 对齐到主力合约的交易日期
rb_main = rb_main.set_index('trade_date')
rb_second = rb_second.set_index('trade_date')
# 合并并前向填充
combined = pd.concat([rb_main['close'], rb_second['close']], axis=1)
combined.columns = ['main_close', 'second_close']
combined = combined.ffill().dropna()
你想想看,如果两个合约的交易日差了1天,直接合并会怎样?对,会出现错位。所以对齐这一步,千万别省。
3.3 复权处理:别让换月吃掉你的利润
期货和股票不一样,合约会到期。主力合约每个月都会换一次,价格会有跳空。
如果不复权,你的回测会看到莫名其妙的“暴涨暴跌”——其实只是换月导致的价差。
常用的复权方法有两种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 前复权 | 调整历史价格,使换月点前后连续 | 趋势跟踪策略(最常用) |
| 后复权 | 调整当前价格,保持历史不变 | 套利策略或价差分析 |
| 不做复权 | 直接拼接,保留跳空 | 高频交易或日内策略 |
我个人习惯用前复权。具体做法是:找到换月那天的价差,然后往前调整所有历史价格。
小技巧:复权因子可以保存下来,方便以后还原原始价格。我一般会在数据库里加一列adjust_factor,这样随时可以切换复权/不复权。
复权代码示例:
def adjust_price(df, roll_date, roll_price_diff):
"""
前复权:将换月前的价格减去价差
df: 包含'close'列的DataFrame
roll_date: 换月日期
roll_price_diff: 换月价差(新合约 - 旧合约)
"""
df = df.copy()
mask = df['trade_date'] < roll_date
df.loc[mask, 'close'] -= roll_price_diff
df.loc[mask, 'open'] -= roll_price_diff
df.loc[mask, 'high'] -= roll_price_diff
df.loc[mask, 'low'] -= roll_price_diff
return df
嗯,这里要注意:换月价差的计算方式有很多种,我习惯用换月前后两天的收盘价差。但如果你做的是跨品种套利,可能需要用结算价。
3.4 数据存储方案:别让数据成为瓶颈
数据存哪里?这个问题看似简单,但选错了后面会非常痛苦。
我推荐以下方案:
- 小规模(< 1GB):直接用CSV或Parquet。简单粗暴,适合个人研究。
- 中规模(1GB - 100GB):用SQLite或MySQL。支持SQL查询,方便做数据切片。
- 大规模(> 100GB):用InfluxDB或ClickHouse。时序数据库,查询速度快一个数量级。
我个人习惯用Parquet + SQLite的组合。为什么?因为Parquet压缩率高,存历史数据;SQLite存最新数据,方便快速查询。
存储结构建议:
data/
├── raw/ # 原始数据,按合约分文件
│ ├── RB.SHF.parquet
│ ├── I.DCE.parquet
│ └── ...
├── processed/ # 清洗后的数据,按品种分文件
│ ├── rb_main.parquet
│ ├── i_main.parquet
│ └── ...
└── factors/ # 复权因子,单独存储
└── adjust_factors.parquet
我曾经见过有人把所有数据塞进一个CSV文件,结果每次回测都要读10分钟……后来我改用Parquet,读取速度提升了20倍。你想想看,如果每次迭代策略都要等10分钟,一天能试几次?
3.5 本章知识体系总览
下面这张图,是我自己画的数据处理流程。每次做新策略,我都会对着这张图检查一遍,确保每个环节都没漏。
我的经验:数据处理的代码,一定要写成函数或类,方便复用。我早期就是每次新策略都重写一遍数据清洗,后来封装成一个DataPipeline类,改个参数就能跑不同品种,效率提升了好几倍。
好了,数据这块就聊到这儿。记住一句话:数据质量决定策略上限。你花在数据上的每一分钟,都会在回测和实盘中加倍回报给你。