第三章:数据获取与清洗——基差交易的第一道坎

说实话,很多做量化的人,都栽在数据上。模型再漂亮,策略再精巧,数据是脏的,结果就是垃圾。我见过太多人花三个月写策略,结果数据对齐没做好,回测曲线漂亮得像假的一样——嗯,其实就是假的。

这一章,咱们就聊聊怎么拿到靠谱的期货和现货数据,怎么把它们对齐,怎么处理那些烦人的缺失值。说白了,就是给基差交易模型打好地基。

3.1 数据源的选择:tushare vs akshare

我个人习惯用两个库:tushareakshare。它们各有千秋,我一般搭配着用。

特性 tushare akshare
数据覆盖 期货、股票、基金等 期货、现货、宏观数据等
数据质量 较高,需积分 中等,免费
接口稳定性 稳定 偶尔变动
适用场景 主力合约、历史数据 现货指数、实时行情
我的小建议: 如果你做的是高频基差交易,tushare 的 tick 级别数据更靠谱。如果是日频策略,akshare 免费够用。

3.2 获取期货行情数据

先拿期货数据。我一般用 tushare 的 fut_daily 接口,它能拿到所有合约的日线数据。但这里有个坑——主力合约怎么处理?

你想想看,基差交易的核心是近月合约和远月合约的价差。如果直接用所有合约,数据量太大,而且流动性差的合约会引入噪音。我的做法是:只取主力合约和次主力合约。

import tushare as ts
import pandas as pd

# 设置token
ts.set_token('your_token_here')
pro = ts.pro_api()

# 获取螺纹钢期货所有合约
df = pro.fut_daily(
    ts_code='RB.SHF',  # 螺纹钢
    start_date='20230101',
    end_date='20231231',
    fields='ts_code,trade_date,close,volume,oi'
)

# 筛选主力合约(按持仓量排序)
df['rank'] = df.groupby('trade_date')['oi'].rank(ascending=False)
main_contract = df[df['rank'] == 1]  # 主力合约
sub_contract = df[df['rank'] == 2]  # 次主力合约

print(f"主力合约数量: {len(main_contract)}")
print(f"次主力合约数量: {len(sub_contract)}")
注意: 主力合约切换时,会有跳空。我曾经因为没处理这个,回测时年化收益率虚高了20%。一定要做复权处理,或者用连续合约。

3.3 获取现货行情数据

现货数据,我推荐用 akshare。它有个好处——能直接拿到商品指数的现货价格,比如南华商品指数、螺纹钢现货价格等。

import akshare as ak

# 获取螺纹钢现货价格(上海地区)
spot_df = ak.spot_price(
    symbol="螺纹钢",
    region="上海",
    start_date="20230101",
    end_date="20231231"
)

# 获取南华商品指数
index_df = ak.index_daily(
    symbol="NHCI",  # 南华商品指数
    start_date="20230101",
    end_date="20231231"
)

print(spot_df.head())
print(index_df.head())

这里有个细节:现货数据通常是日频,而且节假日不更新。但期货市场有夜盘,所以日期上会有差异。嗯,这就是数据对齐的难点所在。

3.4 数据对齐:时间戳的精确匹配

数据对齐,说白了就是让期货和现货的数据在同一个时间点上。我遇到过最头疼的情况:期货有夜盘,现货没有。结果就是,期货的日期比现货多一天。

举个例子:螺纹钢期货夜盘是晚上9点到11点,交易的是第二天的日期。但现货价格还是当天的。如果你直接按日期合并,就会错位。

我的解决方案是:统一使用自然日对齐,把夜盘数据归到当天的日期上。

# 假设期货数据有夜盘,日期需要调整
futures_df['trade_date'] = pd.to_datetime(futures_df['trade_date'])
spot_df['date'] = pd.to_datetime(spot_df['date'])

# 对齐:只取两个数据集都有的日期
common_dates = set(futures_df['trade_date']).intersection(set(spot_df['date']))
futures_aligned = futures_df[futures_df['trade_date'].isin(common_dates)]
spot_aligned = spot_df[spot_df['date'].isin(common_dates)]

# 按日期排序
futures_aligned = futures_aligned.sort_values('trade_date')
spot_aligned = spot_aligned.sort_values('date')

# 合并
merged_df = pd.merge(
    futures_aligned,
    spot_aligned,
    left_on='trade_date',
    right_on='date',
    how='inner'
)

print(f"对齐后数据量: {len(merged_df)}")
核心原则: 宁可少数据,不要错数据。对齐后的数据量如果少了10%,没关系。但如果多了10%,那一定有问题。

3.5 缺失值处理:别轻易填充

缺失值,是数据清洗里最烦人的部分。我见过有人直接用前值填充,结果回测时基差突然变成0,策略直接爆仓。

我的处理原则是:

  • 连续缺失不超过3天:用线性插值
  • 连续缺失超过3天:直接删除该时间段
  • 节假日缺失:保留NaN,不填充
# 检查缺失值
print(merged_df.isnull().sum())

# 线性插值(连续缺失不超过3天)
merged_df['close_futures'] = merged_df['close_futures'].interpolate(
    method='linear',
    limit=3
)

# 删除连续缺失超过3天的行
merged_df = merged_df.dropna(subset=['close_futures', 'close_spot'])

print(f"处理后数据量: {len(merged_df)}")
我曾经踩过的坑: 有一次,我用前值填充了缺失的现货数据,结果那个缺失正好是节假日,现货价格没变,但期货价格波动很大。基差突然缩小,策略开了反向仓位,亏了5%。从那以后,我再也不敢随便填充了。

3.6 知识体系总览

这一章的内容,我画了个图,方便你理解整个流程:

数据获取与清洗流程 数据源选择 期货行情数据 现货行情数据 数据清洗 数据清洗 数据对齐(时间戳匹配) 缺失值处理 最终输出:对齐后的期货+现货DataFrame

3.7 实战中的避坑指南

最后,分享几个我实战中总结的经验:

  • 数据源不要只用一家:我习惯用 tushare 拿期货,akshare 拿现货,交叉验证。如果两个数据源对不上,那肯定有一个有问题。
  • 节假日处理要小心:期货有夜盘,现货没有。节假日前后,数据容易错位。我一般会手动检查春节、国庆前后的数据。
  • 保存原始数据:清洗后的数据不要覆盖原始数据。我习惯在文件名上加 _raw_cleaned 后缀,方便回溯。
一个小技巧: 数据对齐后,可以画个基差图看看。如果基差突然出现异常大的跳空,那大概率是数据没对齐。我每次清洗完数据,都会先画图,再跑策略。

好了,数据获取与清洗就聊到这儿。下一章,咱们会深入基差的计算与特征工程。记住:数据是量化交易的血液,血液干净了,策略才能跑得稳。

专注资料整理