第四章:数据获取与清洗——量化交易的“原材料”处理

做量化交易,说白了就是跟数据打交道。你策略再牛,模型再花哨,数据一塌糊涂,结果就是垃圾进垃圾出。我刚开始做能源期货的时候,就吃过这个亏——拿到的数据没仔细清洗,回测曲线漂亮得不行,实盘一跑直接崩了。嗯,从那以后,我对数据清洗这件事,再也不敢马虎。

这一章,咱们就聊聊怎么拿到期货历史数据,怎么把它洗干净,怎么调整时间频率。我会用 yfinancebaostock 这两个库,带你走一遍完整流程。

4.1 数据源的选择:yfinance vs baostock

先说说这两个工具的区别。我个人习惯是:国内期货用 baostock,国际品种用 yfinance。为什么?

  • yfinance:雅虎财经的接口,覆盖全球市场。原油、天然气、燃料油这些国际能源期货,它都有。但有个坑——数据偶尔会断,尤其是节假日前后。
  • baostock:国内的数据平台,对A股和国内期货支持很好。数据质量稳定,而且免费。缺点嘛,国际品种就别想了。
我的建议:如果你做的是上海国际能源交易中心(INE)的原油期货,或者大连商品交易所的焦煤、焦炭,优先用 baostock。如果是布伦特原油、WTI 原油,那就用 yfinance。

4.2 用 baostock 获取国内期货数据

baostock 的用法其实挺简单的。先登录,然后指定品种代码和日期范围就行。我拿螺纹钢期货举个例子——虽然它不是能源品种,但流程完全一样。

import baostock as bs
import pandas as pd

# 登录
lg = bs.login()
print('登录成功' if lg.error_code == '0' else f'登录失败: {lg.error_msg}')

# 获取螺纹钢期货日线数据
rs = bs.query_history_k_data_plus(
    "rb9999",  # 螺纹钢连续合约
    "date,open,high,low,close,volume,amount",
    start_date='2020-01-01',
    end_date='2023-12-31',
    frequency="d",  # 日线
    adjustflag="2"  # 不复权
)

# 转成 DataFrame
data_list = []
while (rs.error_code == '0') & rs.next():
    data_list.append(rs.get_row_data())
df = pd.DataFrame(data_list, columns=rs.fields)

# 登出
bs.logout()

这里有个细节要注意:rb9999 是螺纹钢的连续合约代码。连续合约是交易所把不同月份的合约拼接起来的,方便做历史回测。你想想看,如果直接用主力合约,每个月换月的时候数据会跳变,回测结果根本不准。

避坑指南:我曾经用主力合约直接做回测,结果换月那天收益率突然跳了5个点,查了半天才发现是合约切换导致的。后来我改用连续合约,或者自己写换月逻辑,才解决了这个问题。

4.3 用 yfinance 获取国际能源期货数据

国际品种的话,yfinance 是首选。代码写法更简洁,但要注意 ticker 的格式。

import yfinance as yf

# 下载 WTI 原油期货数据
ticker = "CL=F"  # WTI 原油连续合约
df = yf.download(ticker, start="2020-01-01", end="2023-12-31")

# 查看前几行
print(df.head())

常见的能源期货 ticker:

品种 Ticker 交易所
WTI 原油 CL=F NYMEX
布伦特原油 BZ=F ICE
天然气 NG=F NYMEX
燃料油 HO=F NYMEX

yfinance 返回的数据是带时区的,默认是 UTC 时间。如果你在国内做交易,记得转成北京时间。

df.index = df.index.tz_convert('Asia/Shanghai')

4.4 数据清洗:处理缺失值和异常值

数据拿到手,第一件事不是分析,是清洗。我见过太多人直接拿原始数据跑模型,结果被几个异常值带偏了方向。

4.4.1 缺失值处理

期货数据缺失的原因很多:节假日休市、数据源中断、合约换月等等。常见的处理方式有三种:

  • 删除法:直接删掉有缺失的行。简单粗暴,但会损失数据量。
  • 向前填充:用上一个非缺失值填充。适合非交易日的缺失。
  • 插值法:用前后值做线性插值。适合短时间内的数据断裂。
# 检查缺失值
print(df.isnull().sum())

# 方法1:删除缺失值
df_clean = df.dropna()

# 方法2:向前填充(适合非交易日)
df_ffill = df.fillna(method='ffill')

# 方法3:线性插值
df_interp = df.interpolate(method='linear')
我的经验:对于日线数据,我一般用向前填充。因为期货市场非交易日很少连续超过3天,向前填充基本能还原真实情况。但如果是分钟级数据,我会用插值法,因为分钟级别的缺失可能发生在交易时段内,向前填充会失真。

4.4.2 异常值检测

异常值怎么找?最常用的方法是 Z-scoreIQR。我个人偏爱 IQR,因为它不受极端值影响。

# 用 IQR 检测收盘价的异常值
Q1 = df['Close'].quantile(0.25)
Q3 = df['Close'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 标记异常值
df['is_outlier'] = (df['Close'] < lower_bound) | (df['Close'] > upper_bound)

# 查看异常值
print(df[df['is_outlier']])

为什么会这样?因为期货市场偶尔会出现“胖手指”交易,或者数据源报错,导致价格瞬间偏离正常范围。这些异常值如果不处理,模型会学到错误规律。

避坑指南:我曾经在回测中没处理异常值,结果模型在某个极端行情日赚了20%,我以为策略无敌了。后来发现那天的数据是数据源报错,价格翻了3倍又跌回来。从那以后,我每次清洗数据都会做异常值检测。

4.5 数据重采样:调整时间频率

很多时候,我们拿到的数据是日线的,但策略需要周线或者小时线。这时候就要做重采样。

# 日线转周线
df_weekly = df.resample('W').agg({
    'Open': 'first',
    'High': 'max',
    'Low': 'min',
    'Close': 'last',
    'Volume': 'sum'
})

# 日线转月线
df_monthly = df.resample('M').agg({
    'Open': 'first',
    'High': 'max',
    'Low': 'min',
    'Close': 'last',
    'Volume': 'sum'
})

重采样的逻辑很简单:开盘价取第一个,最高价取最大值,最低价取最小值,收盘价取最后一个,成交量求和。嗯,这里要注意,如果数据有缺失,重采样前最好先填充,否则结果会偏小。

4.6 本章知识体系

下面这张图,是我梳理的数据获取与清洗的完整流程。你可以把它当作操作手册,每次做数据预处理时对照着来。

数据获取与清洗流程 数据获取 baostock(国内期货) yfinance(国际期货) 数据清洗 缺失值处理 异常值检测 数据重采样 干净可用的数据

这张图把整个流程串起来了。你从左边选数据源,中间做清洗,最后得到干净数据。每一步都有对应的代码实现,照着做就行。

4.7 小结

这一章的内容,说白了就是三件事:拿数据、洗数据、调频率。baostock 和 yfinance 是两个好用的工具,但数据源本身不保证质量,清洗才是关键。缺失值、异常值、重采样,这三个坑踩过去,你的数据才算真正能用。

我个人习惯是:每次拿到新数据,先跑一遍缺失值检查,再跑一遍异常值检测,最后根据策略需求做重采样。这套流程走下来,数据质量基本有保障。

核心要点:
  • 国内期货用 baostock,国际期货用 yfinance
  • 缺失值处理:向前填充适合日线,插值法适合分钟线
  • 异常值检测:IQR 方法比 Z-score 更稳健
  • 重采样时注意:开盘价取第一个,收盘价取最后一个

公众号:蓝海数据掘金营,微信deep3321