第四章:数据获取与清洗——用Python搞定期权链数据
做期权波动率套利,数据就是你的弹药库。弹药不行,枪法再好也白搭。
这一章,我们聊聊怎么用Python把期权链数据搞到手,再把它洗得干干净净。我做了这么多年量化,见过太多人策略写得漂亮,结果数据一塌糊涂,最后亏得莫名其妙。
4.1 期权链数据长什么样?
先搞清楚我们要拿什么数据。期权链,说白了就是同一标的、同一到期日的所有期权合约的集合。
一个典型的期权链数据包含这些字段:
| 字段 | 说明 | 示例 |
|---|---|---|
| code | 合约代码 | 510050C2306M02800 |
| trade_date | 交易日期 | 2023-06-15 |
| expire_date | 到期日 | 2023-06-28 |
| strike | 行权价 | 2.800 |
| call_put | 看涨/看跌 | C / P |
| close | 收盘价 | 0.0456 |
| volume | 成交量 | 12345 |
| oi | 持仓量 | 67890 |
| iv | 隐含波动率 | 0.185 |
| delta | Delta | 0.523 |
嗯,这里要注意:不同数据源给的字段名可能不一样。我习惯统一成上面这套命名,后面处理起来省心。
4.2 用Python获取期权链数据
国内期权数据源,我常用的是Tushare和AkShare。Tushare需要token,AkShare免费但偶尔抽风。
我个人习惯用Tushare做主力数据源,AkShare做备胎。为什么?Tushare的数据质量更稳定,尤其是历史数据回溯时,缺失值少很多。
下面是一个获取50ETF期权链数据的示例:
import tushare as ts
import pandas as pd
from datetime import datetime, timedelta
# 初始化Tushare
pro = ts.pro_api('your_token_here')
def get_option_chain(ts_code, trade_date):
"""
获取某标的某日的期权链数据
"""
df = pro.opt_daily(
ts_code=ts_code,
trade_date=trade_date,
fields='ts_code,trade_date,expire_date,strike,call_put,close,vol,oi,iv,delta'
)
return df
# 获取2023年6月15日50ETF期权数据
df = get_option_chain('510050.SH', '20230615')
print(df.head())
你想想看,如果每天只拿一天的数据,那得跑到猴年马月?所以我们要批量拉取。
def batch_get_option_chain(ts_code, start_date, end_date):
"""
批量获取一段时间内的期权链数据
"""
date_list = pd.date_range(start=start_date, end=end_date, freq='B')
all_data = []
for date in date_list:
date_str = date.strftime('%Y%m%d')
try:
df = get_option_chain(ts_code, date_str)
if df is not None and not df.empty:
all_data.append(df)
print(f'{date_str} 获取成功,共{len(df)}条记录')
except Exception as e:
print(f'{date_str} 获取失败:{e}')
continue
return pd.concat(all_data, ignore_index=True)
# 拉取2023年1月到6月的数据
df_all = batch_get_option_chain('510050.SH', '20230101', '20230630')
4.3 数据清洗——把脏数据揪出来
数据拿到手,别急着用。先看看有没有坑。
常见的坑有三个:
- 缺失值:某天某合约没成交,close是NaN
- 异常值:价格突然跳上天,或者成交量少得离谱
- 重复值:同一合约同一天出现两条记录
我的清洗流程是这样的:
def clean_option_data(df):
"""
清洗期权链数据
"""
# 1. 去重
df = df.drop_duplicates(subset=['ts_code', 'trade_date', 'strike', 'call_put'])
# 2. 处理缺失值
# 成交量或持仓量为0的,价格可能不靠谱
df = df[df['vol'] > 0]
df = df[df['oi'] > 0]
# 3. 处理异常价格
# 价格不能为负,也不能太离谱
df = df[df['close'] > 0]
df = df[df['close'] < df['strike'] * 0.5] # 价格不超过行权价的一半
# 4. 处理异常隐含波动率
df = df[df['iv'] > 0.05] # IV不低于5%
df = df[df['iv'] < 0.80] # IV不高于80%
# 5. 删除到期日当天的数据(流动性差)
df = df[df['trade_date'] != df['expire_date']]
return df.reset_index(drop=True)
df_clean = clean_option_data(df_all)
print(f'清洗前:{len(df_all)}条,清洗后:{len(df_clean)}条')
4.4 构建波动率数据库
数据洗干净了,接下来要存起来。我推荐用SQLite,轻量、便携、不用装服务器。
数据库结构设计很关键。我踩过坑——一开始把所有字段塞一张表,结果查询慢得要死。后来拆成两张表:
- option_daily:日频数据,存价格、成交量、持仓量
- option_greeks:日频数据,存隐含波动率、希腊字母
为什么要拆?因为有些场景只需要价格,有些场景只需要IV。拆开之后,查询速度快了不止一倍。
import sqlite3
def build_option_db(df, db_path='option_data.db'):
"""
构建期权数据库
"""
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 创建日频数据表
cursor.execute('''
CREATE TABLE IF NOT EXISTS option_daily (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts_code TEXT,
trade_date TEXT,
expire_date TEXT,
strike REAL,
call_put TEXT,
close REAL,
volume INTEGER,
oi INTEGER,
UNIQUE(ts_code, trade_date, strike, call_put)
)
''')
# 创建希腊字母表
cursor.execute('''
CREATE TABLE IF NOT EXISTS option_greeks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts_code TEXT,
trade_date TEXT,
strike REAL,
call_put TEXT,
iv REAL,
delta REAL,
gamma REAL,
theta REAL,
vega REAL,
UNIQUE(ts_code, trade_date, strike, call_put)
)
''')
# 插入数据
df[['ts_code', 'trade_date', 'expire_date', 'strike',
'call_put', 'close', 'vol', 'oi']].to_sql(
'option_daily', conn, if_exists='append', index=False
)
df[['ts_code', 'trade_date', 'strike', 'call_put',
'iv', 'delta']].to_sql(
'option_greeks', conn, if_exists='append', index=False
)
conn.commit()
conn.close()
print(f'数据已存入 {db_path}')
build_option_db(df_clean)
4.5 数据质量检查——别让脏数据毁了你的策略
数据存进去了,不代表就万事大吉。我每次入库后都会跑一遍质量检查。
检查哪些东西?
- 时间连续性:每个合约的交易天数是否连续?有没有跳空?
- 价格合理性:看涨期权价格是否随行权价递增?看跌期权是否递减?
- 波动率曲面:同一到期日不同行权价的IV是否平滑?有没有尖刺?
举个例子,检查看涨期权价格是否合理:
def check_call_price_consistency(df):
"""
检查看涨期权价格是否随行权价递增
"""
calls = df[df['call_put'] == 'C']
for date in calls['trade_date'].unique():
daily = calls[calls['trade_date'] == date]
daily = daily.sort_values('strike')
prices = daily['close'].values
# 检查是否严格递减(行权价越高,看涨期权越便宜)
for i in range(len(prices) - 1):
if prices[i] < prices[i+1]:
print(f'{date} 存在价格异常:行权价{daily.iloc[i]["strike"]}的期权价格低于行权价{daily.iloc[i+1]["strike"]}')
break
为什么会这样?说白了,如果看涨期权价格不随行权价递增,那说明数据有问题,或者市场出现了套利机会。但99%的情况是数据问题。
4.6 本章知识体系
下面这张图,把整个数据获取与清洗的流程串起来了:
这张图把整个流程串起来了。从数据获取开始,经过清洗、入库、质量检查,最后得到一个干净的波动率数据库。每一步都有坑,但踩过一次就记住了。
好了,数据到手、洗好、入库。下一章我们就要用这些数据来做波动率计算了。嗯,到时候你会发现,数据质量直接决定了波动率曲面的平滑度——数据脏,曲面就丑,策略就亏。
公众号:蓝海资料掘金营,微信deep3321