第四章:数据获取与清洗——量化交易的“原材料”处理
做量化交易,说白了就是跟数据打交道。你策略再牛,模型再花哨,数据一塌糊涂,结果就是垃圾进垃圾出。我刚开始做能源期货的时候,就吃过这个亏——拿到的数据没仔细清洗,回测曲线漂亮得不行,实盘一跑直接崩了。嗯,从那以后,我对数据清洗这件事,再也不敢马虎。
这一章,咱们就聊聊怎么拿到期货历史数据,怎么把它洗干净,怎么调整时间频率。我会用 yfinance 和 baostock 这两个库,带你走一遍完整流程。
4.1 数据源的选择:yfinance vs baostock
先说说这两个工具的区别。我个人习惯是:国内期货用 baostock,国际品种用 yfinance。为什么?
- yfinance:雅虎财经的接口,覆盖全球市场。原油、天然气、燃料油这些国际能源期货,它都有。但有个坑——数据偶尔会断,尤其是节假日前后。
- baostock:国内的数据平台,对A股和国内期货支持很好。数据质量稳定,而且免费。缺点嘛,国际品种就别想了。
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 是螺纹钢的连续合约代码。连续合约是交易所把不同月份的合约拼接起来的,方便做历史回测。你想想看,如果直接用主力合约,每个月换月的时候数据会跳变,回测结果根本不准。
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')
4.4.2 异常值检测
异常值怎么找?最常用的方法是 Z-score 和 IQR。我个人偏爱 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']])
为什么会这样?因为期货市场偶尔会出现“胖手指”交易,或者数据源报错,导致价格瞬间偏离正常范围。这些异常值如果不处理,模型会学到错误规律。
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 本章知识体系
下面这张图,是我梳理的数据获取与清洗的完整流程。你可以把它当作操作手册,每次做数据预处理时对照着来。
这张图把整个流程串起来了。你从左边选数据源,中间做清洗,最后得到干净数据。每一步都有对应的代码实现,照着做就行。
4.7 小结
这一章的内容,说白了就是三件事:拿数据、洗数据、调频率。baostock 和 yfinance 是两个好用的工具,但数据源本身不保证质量,清洗才是关键。缺失值、异常值、重采样,这三个坑踩过去,你的数据才算真正能用。
我个人习惯是:每次拿到新数据,先跑一遍缺失值检查,再跑一遍异常值检测,最后根据策略需求做重采样。这套流程走下来,数据质量基本有保障。
- 国内期货用 baostock,国际期货用 yfinance
- 缺失值处理:向前填充适合日线,插值法适合分钟线
- 异常值检测:IQR 方法比 Z-score 更稳健
- 重采样时注意:开盘价取第一个,收盘价取最后一个
公众号:蓝海数据掘金营,微信deep3321