第三章:数据获取与处理——量化交易的“原材料”工程

做量化交易,尤其是商品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 数据清洗与对齐:脏数据比没数据更可怕

拿到原始数据后,别急着用。先做三件事:

  1. 去重:同一个交易日出现两条记录?保留第一条,删掉重复的。
  2. 补缺失:遇到停牌或节假日,用前值填充(ffill),或者直接删除。
  3. 对齐时间戳:不同合约的交易时间可能不同,必须统一到同一个时间轴上。

警告:千万不要用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 本章知识体系总览

下面这张图,是我自己画的数据处理流程。每次做新策略,我都会对着这张图检查一遍,确保每个环节都没漏。

数据获取与处理流程 数据获取 Tushare / Wind / 交易所 数据清洗 去重 → 补缺失 → 对齐 复权处理 前复权 / 后复权 / 不复权 数据存储 Parquet / SQLite / ClickHouse 策略回测 原始数据 脏数据 连续价格 可用数据

我的经验:数据处理的代码,一定要写成函数或类,方便复用。我早期就是每次新策略都重写一遍数据清洗,后来封装成一个DataPipeline类,改个参数就能跑不同品种,效率提升了好几倍。

好了,数据这块就聊到这儿。记住一句话:数据质量决定策略上限。你花在数据上的每一分钟,都会在回测和实盘中加倍回报给你。


蓝海数据掘金营,专注资料整理