第四章:数据获取与清洗——套利系统的第一道关卡

做套利交易这么多年,我越来越觉得:策略模型再漂亮,数据不行就是白搭。说白了,数据获取与清洗就是整个套利系统的地基。地基不稳,楼盖得再高也得塌。

今天我们就来聊聊,怎么用Python把期货数据搞到手,再把它收拾得干干净净。

4.1 数据获取:从哪儿拿数据?

国内做期货量化,常用的数据源就两个:tushareakshare。我个人习惯两个都备着,一个挂了换另一个。

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()
注意:3倍标准差法对极端行情敏感。如果市场本身波动大(比如股指期货),建议用5倍标准差。我一般会结合涨跌停板来判断:超过涨跌停范围的,大概率是数据错误。

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 本章知识体系

下面这张图,是我做数据清洗时的完整流程。你可以照着这个思路来。

数据获取与清洗流程 数据获取 数据清洗 数据对齐与重采样 tushare / akshare 重试机制 去重 缺失值处理 异常值检测 时间对齐 重采样(周/月) 干净、对齐的期货数据 数据质量决定策略上限,清洗工作值得花时间

4.5 实战小贴士

最后分享几个我踩过的坑:

  • 数据源切换:tushare和akshare的数据格式不一样。我习惯写一个统一的数据接口,把两个源的数据转成相同格式。这样切换数据源时,策略代码不用改。
  • 交易日历:不同交易所的节假日不同。比如上期所和郑商所,节假日安排可能不一样。我一般会维护一个交易日历表,手动核对。
  • 复权处理:期货有换月问题。主力连续合约的跳空缺口需要处理。我一般用后复权,把历史价格调整到当前合约水平。
避坑指南:我曾经在回测时发现价差突然变大,查了半天才发现是数据对齐出了问题。一个品种用了自然日,另一个用了交易日。从此以后,我每次做数据对齐都会打印一下日期范围,确保两边一致。

数据获取与清洗,听起来枯燥,但这是套利系统最关键的环节。数据干净了,策略才能跑得稳。下一章我们会聊到特征工程,到时候这些清洗好的数据就能派上大用场了。


专注资料整理