第16章 数据获取与清洗:如何获取期权市场数据,并进行清洗和预处理
做波动率曲面套利,说白了就是跟数据打交道。我见过太多人策略逻辑写得漂亮,结果一上实盘就崩——十有八九是数据源出了问题。
这一章,我带你走一遍数据获取和清洗的全流程。嗯,都是我在实盘里踩过的坑,你直接拿去用就行。
16.1 数据源的选择:从哪里拿数据?
期权数据不像股票那么方便。国内期权市场的数据源,我归纳下来就三类:
| 数据源类型 | 代表 | 优点 | 缺点 |
|---|---|---|---|
| 交易所直连 | 上交所、深交所 | 最原始、最准确 | 门槛高、费用贵 |
| 金融数据终端 | Wind、Choice、Tushare | 接口成熟、文档全 | 有延迟、字段可能被加工 |
| 第三方量化平台 | 聚宽、米筐、掘金 | 上手快、有回测环境 | 数据深度有限 |
我个人习惯用Wind Pro接口做主力数据源。为什么?因为它覆盖了全市场的期权链数据,而且历史数据回溯比较完整。但要注意——Wind的实时数据有2-3秒延迟,做高频套利的话得用交易所直连。
16.2 数据字段:你需要拿哪些东西?
做波动率曲面,不是把行情数据拉下来就完事。你需要的是完整的期权链快照。我一般要求至少包含以下字段:
- 合约代码:唯一标识,注意区分不同月份和行权价
- 交易时间:精确到毫秒的时间戳
- 最新价:期权合约的成交价
- 行权价:K值,这是曲面横轴
- 剩余到期时间:T值,这是曲面纵轴
- 标的资产价格:比如50ETF的现价
- 无风险利率:一般用Shibor 1M或国债收益率
- 隐含波动率:这个最好自己算,别信第三方给的
- 买卖价差:判断流动性,价差太大的合约直接扔掉
- 持仓量:辅助判断合约活跃度
16.3 数据获取的代码实现
这里给一个我用Python写的通用数据获取框架。你根据自己的数据源改一下接口就行。
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
def fetch_option_chain_data(source='wind', symbol='50ETF', date=None):
"""
获取期权链快照数据
"""
if date is None:
date = datetime.now().strftime('%Y%m%d')
# 这里以Wind接口为例
# 实际使用时替换成你的数据源API
# 1. 获取所有期权合约代码
option_codes = get_all_option_codes(symbol, date)
# 2. 批量拉取行情数据
raw_data = []
for code in option_codes:
quote = get_quote(code, date)
raw_data.append({
'code': code,
'time': quote['time'],
'last_price': quote['last_price'],
'strike': quote['strike_price'],
'expiry': quote['expiry_date'],
'underlying': quote['underlying_price'],
'bid': quote['bid_price'],
'ask': quote['ask_price'],
'open_interest': quote['open_interest']
})
df = pd.DataFrame(raw_data)
return df
你想想看,如果每次只拉一个合约,100个合约就要循环100次,效率太低了。我建议用批量接口,一次拉完整个链。
16.4 数据清洗:脏数据是策略的杀手
数据拿到手,别急着算曲面。先做清洗。我总结了一套「四步清洗法」:
第一步:剔除无效合约
有些合约一天都没成交一笔,持仓量为0,这种直接扔掉。还有快到期的合约(T+0或T+1),流动性极差,也建议剔除。
第二步:处理异常价格
期权价格不能为负,这个不用我说。但还有更隐蔽的问题——买卖价差过大。我一般设定一个阈值:如果(ask - bid) / mid_price > 30%,就认为这个合约流动性太差,直接过滤掉。
第三步:时间对齐
不同合约的成交时间可能不同。比如9:35:01秒,有的合约成交了,有的没成交。你需要做时间对齐——要么用最新价填充,要么用插值法。我个人习惯用「前向填充」,也就是用最近一笔成交价代替。
第四步:计算隐含波动率
这一步最容易被忽视。很多数据源直接给你一个「IV」字段,但你要问自己:它用的定价模型是什么?利率参数是多少?股息率考虑了没有?
我建议自己算IV。用经典的Newton-Raphson方法,迭代求解Black-Scholes公式:
def calculate_implied_volatility(option_price, S, K, T, r, option_type='call'):
"""
使用Newton-Raphson方法计算隐含波动率
"""
from scipy.stats import norm
def bs_price(sigma):
d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
d2 = d1 - sigma*np.sqrt(T)
if option_type == 'call':
return S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)
else:
return K*np.exp(-r*T)*norm.cdf(-d2) - S*norm.cdf(-d1)
# 初始猜测
sigma = 0.3
for i in range(100):
price = bs_price(sigma)
vega = calculate_vega(S, K, T, r, sigma)
diff = price - option_price
if abs(diff) < 1e-6:
break
sigma = sigma - diff / vega
return sigma
16.5 数据预处理:为曲面构建做准备
清洗完的数据,还需要做几步预处理,才能用来构建波动率曲面。
第一步:标准化时间
把剩余到期时间统一转换成「年化」形式。比如还有30天到期,T = 30/365。注意,国内期权市场用自然日还是交易日?我建议用自然日,因为波动率是连续累积的。
第二步:标准化行权价
用「货币化程度」代替绝对行权价。公式是:moneyness = K / S。这样不同标的、不同时间点的合约才能放在一起比较。
第三步:剔除异常IV点
计算完IV后,你会发现有些合约的IV明显偏离周围合约。比如深度实值期权的IV经常异常高。我一般用3倍标准差法剔除:
def remove_outliers(df, column='implied_vol', z_thresh=3):
mean = df[column].mean()
std = df[column].std()
df = df[(df[column] > mean - z_thresh*std) &
(df[column] < mean + z_thresh*std)]
return df
16.6 数据存储:别让清洗白费
清洗好的数据,建议存成HDF5或Parquet格式。为什么不用CSV?因为期权数据量大——一天全市场快照可能有几千条记录,一年下来就是几百万条。CSV读写太慢,而且不支持列式压缩。
我的存储结构是这样的:
# 按日期分文件存储
data/
2024/
01/
2024-01-02.h5
2024-01-03.h5
...
02/
...
# 每个H5文件内按合约类型分组
/50ETF/call/2024-01-02
/50ETF/put/2024-01-02
/300ETF/call/2024-01-02
这样查询某一天的数据,直接读一个文件就行,不用全量扫描。
16.7 数据质量检查清单
每次跑完清洗流程,我都会过一遍这个清单:
- ✅ 所有合约的标的资产价格是否一致?如果不一致,说明时间没对齐
- ✅ 隐含波动率是否在合理范围(5%-80%)?超出这个范围的,大概率是数据问题
- ✅ 同一行权价、不同到期日的合约,IV是否呈现合理的期限结构?如果出现倒挂,先别急着套利,检查数据
- ✅ 买卖价差是否都在阈值以内?价差大的合约,即使有套利机会也吃不到
- ✅ 数据量是否完整?比如某一天突然少了50%的合约,可能是接口出了问题
16.8 本章知识体系
下面这张图,概括了数据获取与清洗的完整流程:
数据获取与清洗,是整个波动率曲面套利策略的基石。你想想看,如果地基没打好,上面盖的房子再漂亮也没用。我见过太多人花90%的时间写策略,只花10%的时间处理数据——结果就是策略在回测里跑得飞起,实盘一塌糊涂。
记住一句话:垃圾进,垃圾出。数据质量决定了你的策略天花板。