第三章 数据获取与处理:打好套利分析的地基
做跨期套利,数据就是你的眼睛。眼睛要是花了,再好的策略也白搭。
这一章,咱们聊聊怎么把期货数据搞到手,怎么处理那些烦人的主力合约切换问题,以及如何把价差序列算得漂漂亮亮。我做了这么多年量化,见过太多人策略写得天花乱坠,结果数据源就有问题——嗯,这种坑我踩过不少。
3.1 数据源的选择:Tushare vs AKShare
国内做期货量化,主流的数据源就两个:Tushare 和 AKShare。我个人习惯两个都备着,一个挂了用另一个。
| 对比项 | Tushare Pro | AKShare |
|---|---|---|
| 数据质量 | 高,经过校验 | 中高,部分数据来自新浪/东财 |
| 获取方式 | 需注册+token | 免费,pip安装即用 |
| 期货数据 | 日线、分钟线、主力合约 | 日线、分钟线、实时行情 |
| 稳定性 | 稳定,有频率限制 | 偶尔接口变动 |
| 适合场景 | 正式策略、回测 | 快速验证、实时监控 |
我的建议: 回测用 Tushare,实盘监控用 AKShare。为什么?Tushare 数据经过清洗,回测结果更可信;AKShare 实时性好,适合做盘中的价差计算。
3.2 主力合约连续数据拼接
做跨期套利,最头疼的就是主力合约切换。你想想看,螺纹钢的主力合约每几个月就换一次,如果不做拼接,你的价差序列就是断的。
我常用的方法有两种:
- 持仓量法: 每天取持仓量最大的合约作为主力
- 成交量法: 每天取成交量最大的合约
实战中,我偏向用持仓量法。为什么?因为持仓量更能反映资金的实际流向。成交量有时候会被短线交易放大,但持仓量骗不了人。
核心逻辑: 主力合约切换时,需要做「价差调整」。比如 RB1901 切换到 RB1905,价差序列不能直接拼接,要计算两个合约之间的价差基准差。
下面是我常用的主力合约拼接代码:
import akshare as ak
import pandas as pd
import numpy as np
def get_main_contract(symbol, start_date, end_date):
"""
获取主力合约连续数据
使用持仓量法判断主力合约
"""
# 获取所有合约列表
all_contracts = ak.futures_contract_detail(symbol=symbol)
# 获取日线数据
df_list = []
for contract in all_contracts['contract']:
try:
df = ak.futures_daily(symbol=contract,
start_date=start_date,
end_date=end_date)
df['contract'] = contract
df_list.append(df)
except:
continue
# 合并所有合约数据
all_data = pd.concat(df_list)
# 按日期分组,取持仓量最大的合约
main_idx = all_data.groupby('date')['hold'].idxmax()
main_data = all_data.loc[main_idx]
return main_data.sort_index()
# 使用示例
rb_main = get_main_contract('RB', '2023-01-01', '2023-12-31')
print(rb_main.head())
避坑指南: 我曾经遇到过一个问题——节假日前后,主力合约的持仓量会异常波动。比如春节前,很多资金会平仓,导致持仓量骤降。这时候用持仓量法可能会选错主力。我的处理方式是:加一个「连续3天持仓量最大」的过滤条件,避免单日异常。
3.3 价差序列计算
有了主力合约数据,接下来就是算价差。跨期套利的价差,说白了就是近月合约价格减去远月合约价格。
但这里有个细节:价差计算的时间对齐。近月和远月的交易时间不完全一致,比如夜盘时段,近月可能先开盘。我习惯的做法是:只取两个合约都有交易的时刻。
def calculate_spread(near_contract, far_contract):
"""
计算跨期价差
near_contract: 近月合约数据
far_contract: 远月合约数据
"""
# 合并两个合约的数据
merged = pd.merge(near_contract, far_contract,
on='date', suffixes=('_near', '_far'))
# 计算价差
merged['spread'] = merged['close_near'] - merged['close_far']
# 计算价差的移动平均和标准差
merged['spread_ma'] = merged['spread'].rolling(window=20).mean()
merged['spread_std'] = merged['spread'].rolling(window=20).std()
# 计算Z-score
merged['z_score'] = (merged['spread'] - merged['spread_ma']) / merged['spread_std']
return merged
# 使用示例
rb_near = get_main_contract('RB', '2023-01-01', '2023-06-30')
rb_far = get_main_contract('RB', '2023-07-01', '2023-12-31')
spread_data = calculate_spread(rb_near, rb_far)
print(spread_data[['date', 'spread', 'z_score']].head())
3.4 数据清洗与异常值处理
数据拿到手,别急着算策略。先做清洗。我见过有人直接用原始数据跑回测,结果收益率高得离谱——后来发现是数据里有几个涨停板的异常值。
常用的清洗方法:
- 缺失值处理: 期货有节假日,缺失是正常的。用前向填充(ffill)或插值法
- 异常值检测: 用3倍标准差法或IQR法
- 跳空处理: 主力合约切换时会有跳空,需要做复权处理
我的经验: 异常值不一定是错误。比如2015年股灾期间,股指期货的价差出现了极端值,但那不是数据错误,是市场真实的恐慌情绪。所以清洗时,我会先标记异常值,再人工判断是否剔除。
def clean_spread_data(df, z_score_threshold=3):
"""
清洗价差数据
"""
# 复制数据,避免修改原数据
cleaned = df.copy()
# 1. 处理缺失值
cleaned['spread'] = cleaned['spread'].fillna(method='ffill')
# 2. 检测异常值(3倍标准差法)
mean = cleaned['spread'].mean()
std = cleaned['spread'].std()
upper_bound = mean + z_score_threshold * std
lower_bound = mean - z_score_threshold * std
# 标记异常值
cleaned['is_outlier'] = (cleaned['spread'] > upper_bound) | \
(cleaned['spread'] < lower_bound)
# 3. 处理异常值(用中位数替换)
median = cleaned['spread'].median()
cleaned.loc[cleaned['is_outlier'], 'spread'] = median
# 4. 主力合约切换跳空处理
cleaned['spread_adjusted'] = cleaned['spread'] - \
cleaned['spread'].shift(1)
cleaned['spread_adjusted'] = cleaned['spread_adjusted'].fillna(0)
return cleaned
# 使用示例
cleaned_data = clean_spread_data(spread_data)
print(f"原始数据量: {len(spread_data)}")
print(f"清洗后数据量: {len(cleaned_data)}")
print(f"异常值数量: {cleaned_data['is_outlier'].sum()}")
3.5 数据处理的完整流程
下面这张图,是我做数据处理时的标准流程。每次拿到新数据,我都会走一遍这个流程。
3.6 实战中的注意事项
小技巧: 数据下载后,先做一次「数据完整性检查」。比如检查日期是否连续,有没有缺失的交易日。我习惯用 pandas 的 date_range 生成完整的交易日历,然后和实际数据做对比。
重要提醒: 期货数据有「涨跌停板」限制。当价格触及涨跌停时,当天的价差计算会失真。我的处理方式是:如果近月或远月合约触及涨跌停,当天的价差数据标记为「不可用」,不参与策略计算。
好了,数据获取和处理这部分就聊到这儿。记住一句话:数据质量决定策略上限。花80%的时间在数据处理上,剩下20%的时间写策略,这才是量化交易的正确姿势。
下一章,咱们聊聊价差序列的统计特征分析——怎么判断价差是否具有均值回复性,这可是跨期套利策略的核心。