第四章:数据获取与清洗——套利系统的第一道关卡
做套利交易这么多年,我越来越觉得:策略模型再漂亮,数据不行就是白搭。说白了,数据获取与清洗就是整个套利系统的地基。地基不稳,楼盖得再高也得塌。
今天我们就来聊聊,怎么用Python把期货数据搞到手,再把它收拾得干干净净。
4.1 数据获取:从哪儿拿数据?
国内做期货量化,常用的数据源就两个:tushare 和 akshare。我个人习惯两个都备着,一个挂了换另一个。
4.1.1 用tushare拿期货数据
tushare是老牌数据接口,数据质量不错。不过需要注册获取token。嗯,这里要注意,免费版有调用次数限制。
import tushare as ts
# 初始化(记得换成你自己的token)
ts.set_token('你的token')
pro = ts.pro_api()
# 获取螺纹钢主力连续合约日线数据
df = pro.fut_daily(
ts_code='RB9999.XSGE', # 螺纹钢主力连续
start_date='20230101',
end_date='20231231',
fields='trade_date,open,high,low,close,vol'
)
print(df.head())
我在项目中遇到过一个问题:tushare的期货数据有时会缺失某些日期。特别是节假日前后,数据容易断档。所以拿到数据后,我习惯先看一眼日期是否连续。
4.1.2 用akshare拿数据
akshare是后来崛起的库,完全免费,数据源来自各大交易所官网。它的好处是不用注册,拿来就用。
import akshare as ak
# 获取沪铜期货主力合约日线
df = ak.futures_main_sina(
symbol='CU0', # 沪铜主力
start_date='20230101',
end_date='20231231'
)
print(df.head())
你想想看,akshare的数据是直接从新浪财经爬的,有时候网络波动会导致请求失败。我一般会加个重试机制:
import time
from functools import wraps
def retry(func, max_retries=3):
@wraps(func)
def wrapper(*args, **kwargs):
for i in range(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
print(f'第{i+1}次请求失败: {e}')
time.sleep(2)
raise Exception('重试次数用完了')
return wrapper
@retry
def get_futures_data():
return ak.futures_main_sina(symbol='CU0')
4.2 数据清洗:把脏数据洗干净
数据拿到手,别急着用。先看看有没有坑。我见过太多人直接拿原始数据跑策略,结果亏得莫名其妙。
4.2.1 去重
有时候因为网络问题,同一个日期的数据会重复获取。去重是第一步。
# 按交易日期去重,保留第一条
df = df.drop_duplicates(subset=['trade_date'], keep='first')
print(f'去重后数据量: {len(df)}')
df['trade_date'].is_unique 快速验证。
4.2.2 缺失值处理
期货数据缺失的原因很多:节假日、停牌、数据源问题。处理方式要看情况。
# 先看看哪些列有缺失
print(df.isnull().sum())
# 方法1:直接删除缺失行(如果缺失不多)
df = df.dropna()
# 方法2:向前填充(适合非交易日的缺失)
df = df.fillna(method='ffill')
# 方法3:插值法(适合少量连续缺失)
df = df.interpolate(method='linear')
我曾经在回测中发现,用向前填充处理缺失值,会导致套利信号出现滞后。后来我改用插值法,效果好了不少。但要注意,如果连续缺失超过3天,插值也不靠谱,不如直接删掉。
4.2.3 异常值检测
期货市场偶尔会出现极端行情,比如涨停跌停。但有些异常值纯粹是数据错误,比如价格突然跳变100倍。
# 用3倍标准差法检测异常
import numpy as np
def detect_outliers(series, n_std=3):
mean = series.mean()
std = series.std()
outliers = (np.abs(series - mean) > n_std * std)
return outliers
# 检测收盘价异常
outliers = detect_outliers(df['close'])
print(f'发现 {outliers.sum()} 个异常值')
# 将异常值替换为前后均值
df.loc[outliers, 'close'] = np.nan
df['close'] = df['close'].interpolate()
4.3 数据对齐与重采样
套利交易的核心是价差。要算价差,两个品种的数据必须对齐到同一个时间点上。这就是数据对齐。
4.3.1 时间对齐
不同期货品种的交易时间可能不一样。比如螺纹钢有夜盘,而有些品种没有。对齐时要注意。
# 假设我们有螺纹钢(RB)和热卷(HC)的数据
rb_df = get_rb_data()
hc_df = get_hc_data()
# 将日期设为索引
rb_df = rb_df.set_index('trade_date')
hc_df = hc_df.set_index('trade_date')
# 取两个数据集的日期交集
common_dates = rb_df.index.intersection(hc_df.index)
rb_aligned = rb_df.loc[common_dates]
hc_aligned = hc_df.loc[common_dates]
# 计算价差
spread = rb_aligned['close'] - hc_aligned['close']
print(spread.head())
我刚开始做套利时,没注意对齐问题。结果价差算出来忽大忽小,回测曲线漂亮得不行,实盘一跑就崩。后来才发现,两个品种的交易日历不一样,有些日期根本没对齐。
4.3.2 重采样
有时候我们需要把日线数据转成周线或月线。重采样就派上用场了。
# 将日线重采样为周线
weekly_df = df.resample('W').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'vol': 'sum'
})
# 将日线重采样为月线
monthly_df = df.resample('M').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'vol': 'sum'
})
4.4 本章知识体系
下面这张图,是我做数据清洗时的完整流程。你可以照着这个思路来。
4.5 实战小贴士
最后分享几个我踩过的坑:
- 数据源切换:tushare和akshare的数据格式不一样。我习惯写一个统一的数据接口,把两个源的数据转成相同格式。这样切换数据源时,策略代码不用改。
- 交易日历:不同交易所的节假日不同。比如上期所和郑商所,节假日安排可能不一样。我一般会维护一个交易日历表,手动核对。
- 复权处理:期货有换月问题。主力连续合约的跳空缺口需要处理。我一般用后复权,把历史价格调整到当前合约水平。
数据获取与清洗,听起来枯燥,但这是套利系统最关键的环节。数据干净了,策略才能跑得稳。下一章我们会聊到特征工程,到时候这些清洗好的数据就能派上大用场了。