2. 市场数据获取:从交易所/数据商获取期权链数据,构建原始波动率散点图

做波动率曲面,第一步不是建模,不是插值,而是——拿数据。

你可能觉得这有什么难的?找个数据商,调个API,数据不就来了吗?嗯,我当年也是这么想的。结果第一次拿到的期权链数据,隐含波动率算出来全是NaN,有的行权价还是负数……那一刻我才明白,数据获取这一步,坑比想象中多。

2.1 数据源的选择:交易所 vs 数据商

先说说数据从哪来。我个人习惯把数据源分成两类:

  • 交易所直连:比如上交所、深交所、CBOE。数据最原始,延迟最低,但你需要自己处理协议、解析报文。适合做高频或做市商团队。
  • 数据商:比如Wind、Bloomberg、Quandl、Polygon。封装好了,调用方便,但成本高,而且数据可能经过「清洗」——说白了就是可能被改过。

做波动率曲面研究,我建议用数据商的数据起步。为什么?因为省时间。你想想看,花一周时间写解析上交所期权的代码,不如直接调Wind接口,把精力放在核心建模上。

我的经验:如果做回测,尽量用数据商的历史数据;如果做实盘,一定要对比交易所原始数据,防止数据商延迟或漏数据。我曾经吃过这个亏,回测跑得漂亮,实盘一跑就崩,最后发现是数据商把最后一笔tick丢了。

2.2 期权链数据结构:你需要哪些字段?

拿到数据后,先看看它长什么样。一个标准的期权链数据,至少包含以下字段:

字段 说明 注意事项
合约代码 唯一标识 注意区分Call/Put
行权价 K 单位要统一(元/美元)
到期日 T 注意是否包含到期日当天
最新价 期权价格 用收盘价还是中间价?
标的资产价格 S 与期权价格时间戳对齐
无风险利率 r 通常用国债收益率
隐含波动率 IV 有些数据商直接提供

这里有个细节:隐含波动率是算出来的,不是直接拿到的。如果数据商直接给了IV,你也要自己验算一遍。为什么?因为不同数据商用的定价模型可能不一样,有的用Black-Scholes,有的用二叉树,算出来的IV会有差异。

2.3 数据清洗:别让脏数据毁了你的曲面

拿到原始数据后,别急着画图。先做清洗。我一般按以下步骤来:

  1. 剔除异常价格:期权价格不能为负,不能低于内在价值。比如一个深度实值Call,价格比S-K还低,那肯定有问题。
  2. 剔除流动性差的合约:成交量太小、买卖价差太大的,直接扔掉。这些数据算出来的IV没有意义。
  3. 剔除临近到期的合约:距离到期日小于3天的,IV波动极大,容易干扰曲面形态。
  4. 剔除深度虚值合约:Delta绝对值小于0.05的,IV往往被高估,而且对模型参数敏感。
避坑指南:我曾经在构建50ETF期权曲面时,没有剔除深度虚值合约,结果曲面尾部翘得老高,看起来像「微笑」变成了「狂笑」。后来发现是那些深度虚值合约的IV被数据商算错了——因为它们的价格太低了,稍微一点误差就会导致IV剧烈波动。

2.4 计算隐含波动率:自己动手,丰衣足食

如果数据商没提供IV,或者你想自己算,那就用Black-Scholes模型反推。代码很简单:

import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq

def black_scholes_price(S, K, T, r, sigma, option_type='call'):
    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':
        price = S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)
    else:
        price = K*np.exp(-r*T)*norm.cdf(-d2) - S*norm.cdf(-d1)
    return price

def implied_volatility(market_price, S, K, T, r, option_type='call'):
    # 用二分法或布伦特法求解
    objective = lambda sigma: black_scholes_price(S, K, T, r, sigma, option_type) - market_price
    return brentq(objective, 0.001, 5.0)

嗯,这里要注意:brentq的上下界要合理。我见过有人设成0到100,结果算出来IV是80%——明显不合理。一般股票期权的IV在10%-60%之间,指数期权可能更低一些。

2.5 构建原始波动率散点图

数据清洗完,IV算好了,接下来就是画图。原始波动率散点图,说白了就是把每个期权的隐含波动率按行权价和到期日画在三维空间里。

我一般用Matplotlib的3D散点图,或者Plotly的交互式图表。代码示例:

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')

# 假设data是清洗后的DataFrame
scatter = ax.scatter(
    data['strike'],          # 行权价
    data['time_to_maturity'], # 剩余期限
    data['implied_vol'],     # 隐含波动率
    c=data['implied_vol'],   # 颜色映射
    cmap='viridis',
    s=20
)

ax.set_xlabel('Strike Price')
ax.set_ylabel('Time to Maturity (years)')
ax.set_zlabel('Implied Volatility')
plt.colorbar(scatter)
plt.show()

画出来的图长什么样?你会看到一堆散点,大致形成一个曲面形状——中间低、两边高,这就是传说中的「波动率微笑」。但注意,原始数据点往往很稀疏,尤其是远月合约和深度实值/虚值合约,数据点少得可怜。

核心要点:原始波动率散点图是后续所有工作的基础。它告诉你:数据覆盖范围够不够?有没有异常点?曲面形态是否合理?我每次拿到新数据,第一件事就是画这个图,盯着看五分钟,心里就有数了。

2.6 一个完整的实战流程

最后,我把整个流程串起来,画了一张流程图,方便你理解:

波动率曲面数据获取与预处理流程 1. 数据源选择 2. 获取期权链数据 3. 数据清洗 剔除异常价格 剔除低流动性 4. 计算隐含波动率 5. 绘制原始散点图 输出:波动率散点图

这张图把整个流程串起来了。你从数据源开始,一步步走到散点图,每一步都有坑,每一步都要小心。但只要你走通了这一步,后面的曲面平滑和插值,就有了坚实的基础。

我的习惯:每次拿到新数据,我都会把清洗前后的散点图放在一起对比。清洗前可能有一堆飞点,清洗后干净多了。这种对比图,我建议你也保留下来——以后写报告、做复盘,都是很好的素材。

好了,数据拿到了,散点图也画出来了。接下来,你看着这张图,心里应该有个大概的判断:这个曲面是微笑还是偏斜?数据覆盖够不够?哪些区域需要重点处理?

嗯,这些判断,就是下一阶段要做的事了。