Skip to content
All library documents

Time-of-Day Volatility in Black–Scholes Option Pricing

Notebook TqSdk

Summary

The document examines whether volatility in the CSI 300 varies by weekday and time of day, then uses that pattern to modify Black–Scholes pricing for an index option. It calculates five-minute log returns from open to close over a year of index data, excluding cross-session movement, and compares dispersion across weekdays and intraday intervals. The analysis reports higher volatility around morning and afternoon opens, with Monday’s opening interval especially elevated.

For the modified model, it estimates periodic return variances and integrates them over the remaining time to expiry to obtain time-varying variance. It compares standard and adjusted model prices with observed option prices, using residual means and log-price residual dispersion as diagnostics. The final two hours are excluded from the pricing comparison because settlement uses an average price. The document provides code and a proposed comparison, but no displayed numerical results; conclusions are limited to one underlying, one option, and the chosen sample and assumptions.

Key ideas

  • The analysis tests whether index volatility varies by weekday and intraday interval using five-minute data.
  • It reports elevated volatility around market openings and particularly high Monday opening volatility.
  • The adjusted pricing approach integrates time-specific return variance over the option’s remaining life.
  • Standard and time-adjusted Black–Scholes prices are compared with observed option prices using residual measures.
  • The last two hours are omitted from the comparison because settlement uses an average price.

Tags

Full text
# 因子计算分析


## 因子计算分析

```python
import datetime

from tqsdk import TqApi, TqAuth
from tqsdk.tafunc import get_t, get_bs_price

import numpy as np
import pandas as pd
import plotly.express as px

pd.options.display.max_rows= None
pd.options.display.max_columns = None
pd.options.display.width = 0
pd.options.display.float_format = '{:.6f}'.format
```

```python
# 标准 BS 定价模型假设波动律为常量, 但是波动率实际和时间相关, 这里扩展 BS 定价模型引入时间相关波动率并与标准模型进行对比
api = TqApi(auth=TqAuth('快期账户', '快期密码'))
quote = api.get_quote('CFFEX.IO2405-C-3550')
underlying = quote.underlying_symbol
exp = datetime.datetime.fromtimestamp(quote.expire_datetime)
```

```python
# 获取沪深300指数过去一年的5分钟现数据,用来分析波动率
csi = api.get_kline_data_series(symbol=underlying, duration_seconds=300, start_dt=exp-datetime.timedelta(days=365), end_dt=exp)
# 将 unix time 转换成 pandas.Datetime 并获得周几及时间信息
csi["dt"] = pd.to_datetime(csi["datetime"], utc=True).dt.tz_convert('Asia/Shanghai')
csi["time"] = csi["dt"].dt.time
csi["week"] = csi["dt"].dt.weekday+1
# p 为每五分钟的波动率, 不计算跨交易时段的波动, 用来分析波动率的周期性
csi["p"] = np.log(csi["close"]/csi["open"])
```

```python
# 可以看到周一到周五的波动率有显著性的差异
px.bar(csi.groupby("week").std(numeric_only=True), y="p")

```

```python
# 每天不同时间段的波动率也有显著性的差异
# 早上及下午开盘的波动率显著偏高
# 注意这里的波动率是收盘价对比开盘价, 而不是收盘价对比上个收盘价, 因此 9:30 开盘的高波动率只包含集合竞价之后的价格波动, 不包含相对昨天收盘的波动
px.bar(csi.groupby("time").std(numeric_only=True), y="p")
```

```python
# 按周一~五绘制出不同时段的波动率, 可以观察到周一的 9:30 又显著高于周二~五
px.line(csi.groupby(["week","time"]).std(numeric_only=True).reset_index(), x="time", y="p", color="week")
```

```python
# 上述信息验证了波动率实际和时间相关的假设, 接下来基于该假设计算改进版的 BS 定价
# Tσ² = ∫σ²(dt)
# 其中 T 是距离到期时间, 按照波动率的周期性积分出改进版波动率
# 这里计算的 r 是使用当前周期的收盘价对比上一周期的收盘价, 使得积分的结果能够完整的覆盖 T
csi["r"] = np.log(csi["close"].shift(1)/csi["close"])
# 得到周一~五不同时间段的周期性收益率方差
csiv = csi.groupby(["week","time"]).var(numeric_only=True).reset_index()[["week", "time", "r"]]
```

```python
# 只关注期权最后一周的行情数据
serial = api.get_kline_serial(symbol=[underlying, quote.instrument_id], duration_seconds=300, data_length=48*5)
serial["dt"] = pd.to_datetime(serial["datetime"], utc=True).dt.tz_convert('Asia/Shanghai')
serial["time"] = serial["dt"].dt.time
serial["week"] = serial["dt"].dt.weekday+1
# 最后一个交易日使用最后两小时的算数平均数作为交割结算价
serial["close_avg"] = serial["close"]
serial.iloc[-24:, serial.columns.get_loc("close_avg")] = serial["close"].iloc[-24:].expanding().mean()
```

```python
# 将 csi 周期性收益率方差 join 期权价量信息, 计算出改进版波动率
serial = serial.merge(csiv, on=["week", "time"])
```

```python
# v 为年化后的改进版波动率
serial["v"] = np.sqrt(serial["r"].iloc[::-1].expanding().mean()*48*250)
```

```python
# 无风险利率 1.5%
rf = 0.015
# 距离到期的时间序列
t = get_t(serial, quote.expire_datetime)
# 行权价格的折现, 用于计算时间价值
serial["present_k"] = quote.strike_price * np.exp(-rf * t)
serial["time_value"] = (serial["close_avg"] - serial["present_k"]) - serial["close1"]
# 计算标准和改进版 BS 模型价格, 以及和实际期权价格的差
# 最后两小时使用算数平均作为交割结算价导致不能简单的使用波动率来估计交割结算价的变化, 因此跳过最后两小时
# v 为标准 BS 模型所用到的波动率
v = np.sqrt(np.var(csi["r"]) * 250*48)
serial["bs"] = get_bs_price(serial["close"].iloc[:-24], k=quote.strike_price, r=rf, v=v, t=t.iloc[:-24], option_class=quote.option_class)
serial["bs_diff"] = serial["bs"] - serial["close1"]
serial["tdbs"] = get_bs_price(serial["close"].iloc[:-24], k=quote.strike_price, r=rf, v=serial["v"].iloc[:-24], t=t.iloc[:-24], option_class=quote.option_class)
serial["tdbs_diff"] = serial["tdbs"] - serial["close1"]
```

```python
# 计算结果
serial[["dt", "close_avg", "time_value", "close1", "bs", "bs_diff", "tdbs", "tdbs_diff"]]
```

```python
# 绘制出时间价值(time_value), 标准 BS 定价与期权价格的差(bs_diff), 改进版 BS 定价与期权价格的差(tdbs_diff)
px.line(serial, x="dt", y=["time_value", "bs_diff", "tdbs_diff"])
```

```python
# 残差均值
serial[["bs_diff", "tdbs_diff"]].mean()
```

```python
# 标准模型的残差标准差
np.log(serial["bs"]/serial["close1"]).std()
```

```python
# 改进模型的残差标准差
np.log(serial["tdbs"]/serial["close1"]).std()
```

```python
api.close()
```

```python

```

Shown in full with attribution under the source's licence. Licence: Apache-2.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.