量化交易中文教材

第 09 章 衍生品与波动率策略

本章把第 08 册(Hull)里的定价公式和希腊字母,变成研究和交易波动率时真正用得上的工具:一个不会在边角情形出错的定价与隐含波动率引擎,一张没有套利的隐含波动率曲面,一套解释期权头寸盈亏来源的分解方法,以及期货展期和尾部对冲的工程细节。核心结论有三条。第一,期权策略的收益几乎都可以写成"实现方差减隐含方差"乘以一个权重,赚的是方差风险溢价,承担的是崩盘风险。第二,期货的"展期收益"不等于预期收益,只有当期货价格系统性偏离未来现货时,carry 才是一个因子。第三,尾部对冲买的是"最差一个月"的保护,不是"最大回撤"的保护,两者的成本和效果差别很大。

学习目标

  • 写出向量化、带边界检查的 BSM 定价、希腊字母和隐含波动率求解器,并知道 IV 在什么时候不可信。
  • 从带买卖价差的期权报价出发,估计远期价和折现因子,拟合每个期限的 SVI,检查日历套利和蝶式套利,并在总方差上做期限插值。
  • 用模拟理解方差风险溢价:为什么卖方差的 Sharpe 看起来很高,而它的风险集中在少数几个月。
  • 用 gamma–theta 公式分解 delta 对冲期权的盈亏,量化对冲频率、交易成本和对冲所用波动率的影响。
  • 正确拼接期货连续合约,把期货收益分解为现货收益加展期收益,并区分"展期收益"和"carry 溢价"。
  • 比较看跌期权、看跌价差和波动率目标三种尾部风险管理方式的成本与效果。

读前导读

这一章在解决什么问题。 把自己放在期权做市台上。做市商每天要做的事,正好是本章的六节:收盘后为几万个合约重算隐含波动率和希腊字母(9.2 节,定价引擎);把杂乱的买卖报价整理成一张平滑、无套利的波动率曲面,作为报价和估值的基准(9.3 节);做市商天然是期权的净卖方,赚的是隐含波动率高于实际波动的那部分,也就是方差风险溢价(9.4 节);每个交易员的 delta 对冲账户每天要解释"今天赚的钱来自哪里",这就是 gamma–theta 分解(9.5 节)。后两节离开期权:9.6 节讲期货换月和 carry,对应商品和股指期货的展期管理;9.7 节讲尾部对冲,对应资产配置委员会讨论"要不要花钱买保护"。

你在 CFA 里学过 BSM 公式、看涨看跌平价、delta 对冲和单步二叉树,也学过持有成本模型(期货价格 \(=\) 现货 \(\times\) 融资成本 \(-\) 便利收益)。本章不重新推导这些,而是讲它们落到代码和数据上最容易出错的地方。可以用一句话概括本章的核心:期权的盈亏几乎都可以写成"实现方差减隐含方差"乘以一个权重,卖期权就是卖保险,平时赚保费,偶尔赔大钱。

需要先想起来的数学。

  • 泰勒展开到二阶。 \(f(S+dS)\approx f(S)+f'(S)dS+\tfrac12f''(S)dS^2\)。对期权价格,一阶项是 delta,二阶项是 gamma。CFA 里债券价格变化 \(\approx-\)久期 \(\times\Delta y+\tfrac12\)凸性 \(\times\Delta y^2\) 是同一个公式。9.5 节的 gamma–theta 分解就是这个展开加上 BSM 方程。参见 第 00 册第 02 章 导数与泰勒展开。
  • 牛顿法求根。 解 \(f(x)=0\) 时,从猜测 \(x_0\) 出发,用切线逼近:\(x_1=x_0-f(x_0)/f'(x_0)\)。例:解 \(x^2=2\),从 \(x_0=1.5\) 出发,\(x_1=1.5-(2.25-2)/3=1.4167\),一步就接近 \(\sqrt2=1.4142\)。求隐含波动率时 \(f'\) 就是 vega。
  • 偏导数与 BSM 方程。 期权价格 \(V(S,t)\) 同时依赖价格和时间,\(\partial V/\partial S\) 是 delta,\(\partial^2V/\partial S^2\) 是 gamma,\(\partial V/\partial t\) 是 theta。BSM 偏微分方程是这三者必须满足的一条关系。参见 第 00 册第 05 章 多元微积分与优化。
  • 正态分布的密度 \(n(x)\) 与累积分布 \(N(x)\)。 \(N(x)\) 是"标准正态小于 \(x\) 的概率",\(n(x)=N'(x)=e^{-x^2/2}/\sqrt{2\pi}\) 是它的导数。gamma 和 vega 里出现的都是 \(n(d_1)\),平值时 \(d_1\approx0\),\(n(0)\approx0.4\),这就是平值期权 gamma 和 vega 最大的原因。
  • 风险中性期望 \(\hat E\) 与现实期望 \(E^P\)。 前者是期权价格隐含的"概率"下的期望,后者是真实世界的期望,两者之差就是风险溢价。CFA 二叉树里的风险中性概率 \(p=(e^{rT}-d)/(u-d)\) 就是前者。参见 第 00 册第 07 章 概率中的分析工具。

怎么读这一章。 9.4 节(方差风险溢价)和 9.5 节(gamma–theta 分解)是核心必读,前者决定你怎么看任何"卖波动率"的产品,后者是理解期权账户盈亏的钥匙。9.2 节如果不写代码,读 9.2.2 的两个工程问题和代码后的解读即可。9.3 节曲面构建最技术化:第一次读抓住三点(用平价回归求远期、只用虚值期权、在总方差上插值),SVI 公式和 Durrleman 条件可以只看结论。9.6 节期货展期对做商品或 CTA 的读者必读,9.6.2 节"展期收益不等于预期收益"是常见误解。9.7 节尾部对冲适合与资产配置决策一起读。

9.1 问题与动机

一个量化团队接触衍生品,通常出于三种需要。其一是把期权当作信息源:隐含波动率、偏斜和期限结构包含市场对未来风险的定价,可以做择时或截面因子的输入。其二是直接交易波动率:卖出期权或方差互换赚取方差风险溢价,或者买入期权做 gamma 交易。其三是用期货和期权管理风险:用股指期货调整 β,用看跌期权或其他工具对冲尾部。

这三件事都依赖同一组基础设施:定价公式、隐含波动率、希腊字母、波动率曲面和连续合约。第 08 册已经推导了这些公式(BSM 见第 08 册第 15a、15b 章,希腊字母见第 19a、19b 章,波动率微笑见第 20 章,方差互换见第 26b 章),本章不再重复推导,而是关注把它们落到代码和数据上时最常出错的地方:报价噪声被放大成离谱的隐含波动率;远期价估错导致整条微笑歪掉;用不同期限的曲面插值时引入套利;把期货的换月跳空当成收益;把一个负偏度极大的策略按 Sharpe 排名。

本章所有数据都是在代码里生成的合成数据。它们保留了真实市场的几个关键特征:GJR-GARCH 的波动聚集和杠杆效应,t 分布的厚尾,稀有的负跳跃,带价差和最小报价单位的期权报价,便利收益率随时间变化的期货曲线。与真实数据的差别在每节末尾说明。任何模拟出来的收益数字都只用于说明机制,不代表真实市场能赚到的钱。

9.2 BSM 与希腊字母:做一个可靠的定价引擎

9.2.1 需要哪些公式

带连续股息率 \(q\) 的欧式期权价格(第 08 册第 15b、17 章)可以统一写成

\[V=\phi\left[S e^{-qT}N(\phi d_1)-K e^{-rT}N(\phi d_2)\right],\qquad d_{1,2}=\frac{\ln(S/K)+(r-q\pm\tfrac12\sigma^2)T}{\sigma\sqrt T},\]

其中 \(\phi=+1\) 为看涨,\(\phi=-1\) 为看跌。用 \(\phi\) 统一写法的好处是代码只有一条路径,看涨看跌不会出现一边改了另一边忘改的错误。希腊字母同样可以统一:\(\Delta=\phi e^{-qT}N(\phi d_1)\),\(\Gamma=e^{-qT}n(d_1)/(S\sigma\sqrt T)\),\(\text{Vega}=Se^{-qT}n(d_1)\sqrt T\),Theta 的表达式见第 08 册第 19b 章。它们必须满足 BSM 偏微分方程

\[\Theta+(r-q)S\Delta+\tfrac12\sigma^2S^2\Gamma=rV,\]

这条恒等式是检查希腊字母实现是否正确的最好工具,比逐个对照公式更可靠。

金融直觉:BSM 方程读成一句话就是"delta 对冲后的组合只赚无风险利率"。把它移项为 \(\Theta+\tfrac12\sigma^2S^2\Gamma=r(V-\Delta S)+q\Delta S\):右边是对冲组合(买 1 份期权、卖空 \(\Delta\) 股)净占用资金 \(V-\Delta S\) 应得的无风险利息,加上卖空股票要替出借方支付的股息,左边是时间流逝的损失(theta)加上价格波动带来的凸性收益(gamma)。做市商的账本每天就是这两项在拉锯:持有正 gamma 的人每天付 theta 作为"租金",换取从价格波动中赚钱的权利。 为什么这条恒等式能查错?delta、gamma、theta 是分别写出的三个公式,任何一个的符号或系数写错,三者就不再满足这条关系,残差会远离 0。代码 9.1 的残差在 \(10^{-14}\),说明三个公式互相一致。这和会计里用试算平衡检查分录是同一个思路:单个科目对不对不好查,借贷平不平一眼可见。

9.2.2 隐含波动率求解

隐含波动率是方程 \(V_{BS}(\sigma)=V_{mkt}\) 的根。\(V_{BS}\) 对 \(\sigma\) 单调递增,所以根唯一,但有两个工程问题。

第一,根不一定存在。价格必须落在无套利区间 \(\big(e^{-rT}(\phi(F-K))^+,\ \text{上界}\big)\) 内,看涨上界为 \(Se^{-qT}\),看跌上界为 \(Ke^{-rT}\)。真实报价经常违反下界(陈旧报价、股息估错、美式期权),求解器必须先检查,而不是让二分法静悄悄地返回区间端点。

第二,根存在也不一定可信。\(\partial V/\partial\sigma\) 就是 vega。深度虚值或临近到期的期权 vega 很小,价格的一个最小报价单位(tick)对应好几个甚至几十个波动率点。这类 IV 不是"算错了",而是信息量本来就少,应该在拟合曲面时降权或剔除。

求解方法上,牛顿法用 vega 做导数,收敛快,但初值不好时会跳出合理区间;二分或 Brent 法稳但慢,而且难以向量化。工程上常用"带括号保护的牛顿法":同时维护一个包含根的区间 \([lo,hi]\),牛顿步落在区间外就改走二分。这样既保留二次收敛,又不会发散,而且每一步都是数组运算,可以一次解几十万个期权。

白话解释:对照代码 implied_vol 看这套设计。第一步先算无套利边界 intrinsic 和 upper,价格不在区间内的直接标记 ok=False,不进入求解。第二步给初值:Brenner–Subrahmanyam 近似说平值期权价格约为 \(0.4\,S\sigma\sqrt T\)(\(0.4\approx1/\sqrt{2\pi}\)),反解得 \(\sigma\approx\sqrt{2\pi/T}\cdot C/S\),这就是代码里的初值公式。第三步循环:每次根据当前价格误差 \(f\) 的正负收紧括号(价格偏低说明根在右边,lo 右移;偏高则 hi 左移),再算牛顿步 \(x-f/\text{vega}\);如果牛顿步跳出括号就改取括号中点。所有期权同时走这几步,用 np.where 代替 if,这就是"向量化"。最后用 vega 下限做二次筛选:vega 太小的结果即使算出来也不可信。 用数字体会 vega 的作用:如果一个期权的 vega 是每波动率点 0.002 元,报价只精确到 0.01 元,那么一个 tick 的价格误差就对应 5 个波动率点的 IV 误差。

9.2.3 代码:定价、希腊字母核对与向量化 IV

# 代码 9.1  向量化 BSM 定价、希腊字母核对与隐含波动率求解
import time
import numpy as np
from scipy.stats import norm
from scipy.optimize import brentq

def bs_price(S, K, T, r, q, sig, cp):
    """cp=+1 看涨, -1 看跌;所有参数可广播。"""
    sqT = np.sqrt(T)
    d1 = (np.log(S / K) + (r - q + 0.5 * sig**2) * T) / (sig * sqT)
    d2 = d1 - sig * sqT
    return cp * (S * np.exp(-q * T) * norm.cdf(cp * d1)
                 - K * np.exp(-r * T) * norm.cdf(cp * d2))

def bs_greeks(S, K, T, r, q, sig, cp):
    sqT = np.sqrt(T)
    d1 = (np.log(S / K) + (r - q + 0.5 * sig**2) * T) / (sig * sqT)
    d2 = d1 - sig * sqT
    disc_q, disc_r = np.exp(-q * T), np.exp(-r * T)
    delta = cp * disc_q * norm.cdf(cp * d1)
    gamma = disc_q * norm.pdf(d1) / (S * sig * sqT)
    vega = S * disc_q * norm.pdf(d1) * sqT              # 每单位 sigma
    theta = (-S * disc_q * norm.pdf(d1) * sig / (2 * sqT)
             - cp * r * K * disc_r * norm.cdf(cp * d2)
             + cp * q * S * disc_q * norm.cdf(cp * d1))  # 每年
    return delta, gamma, vega, theta

def implied_vol(price, S, K, T, r, q, cp, lo=1e-4, hi=5.0, tol=1e-10, maxit=60):
    """向量化的"带括号保护的牛顿法":牛顿步落在括号外就改用二分。
    返回 (iv, ok);ok=False 表示价格越界或 vega 太小,IV 不可信。"""
    price, S, K, T, cp = np.broadcast_arrays(*map(np.asarray, (price, S, K, T, cp)))
    F = S * np.exp((r - q) * T)
    df = np.exp(-r * T)
    intrinsic = df * np.maximum(cp * (F - K), 0.0)
    upper = np.where(cp > 0, S * np.exp(-q * T), K * df)
    ok = (price > intrinsic + 1e-12) & (price < upper)
    lo = np.full(price.shape, lo); hi = np.full(price.shape, hi)
    # 初值:Brenner–Subrahmanyam 平值近似,夹在括号内
    x = np.clip(np.sqrt(2 * np.pi / T) * price / (S * np.exp(-q * T)), 0.05, 2.0)
    for _ in range(maxit):
        f = bs_price(S, K, T, r, q, x, cp) - price
        lo = np.where(f < 0, x, lo); hi = np.where(f > 0, x, hi)
        _, _, v, _ = bs_greeks(S, K, T, r, q, x, cp)
        step = f / np.maximum(v, 1e-300)
        xn = x - step
        bad = (xn <= lo) | (xn >= hi) | ~np.isfinite(xn)
        xn = np.where(bad, 0.5 * (lo + hi), xn)
        if np.nanmax(np.abs(xn - x)[ok], initial=0) < tol:
            x = xn; break
        x = xn
    _, _, v, _ = bs_greeks(S, K, T, r, q, x, cp)
    ok &= v > 1e-6 * S          # vega 太小时,价格的 1 个 tick 对应很大的 IV 误差
    return np.where(ok, x, np.nan), ok

rng = np.random.default_rng(7)
n = 200_000
S = 100.0; r, q = 0.03, 0.01
K = S * np.exp(rng.uniform(-0.5, 0.5, n))
T = rng.uniform(5 / 365, 2.0, n)
sig = rng.uniform(0.08, 0.9, n)
cp = rng.choice([-1, 1], n)
px = bs_price(S, K, T, r, q, sig, cp)

# 1) 平价与希腊字母的数值核对
c = bs_price(S, K, T, r, q, sig, 1); p = bs_price(S, K, T, r, q, sig, -1)
print("put-call parity 最大误差:", np.abs(c - p - (S*np.exp(-q*T) - K*np.exp(-r*T))).max())
d, g, v, th = bs_greeks(S, K, T, r, q, sig, cp)
h = 1e-3
fd_delta = (bs_price(S+h, K, T, r, q, sig, cp) - bs_price(S-h, K, T, r, q, sig, cp)) / (2*h)
fd_gamma = (bs_price(S+h, K, T, r, q, sig, cp) - 2*px + bs_price(S-h, K, T, r, q, sig, cp)) / h**2
fd_vega = (bs_price(S, K, T, r, q, sig+1e-5, cp) - bs_price(S, K, T, r, q, sig-1e-5, cp)) / 2e-5
print("delta/gamma/vega 有限差分最大误差: %.1e %.1e %.1e" % (
    np.abs(fd_delta-d).max(), np.abs(fd_gamma-g).max(), np.abs(fd_vega-v).max()))
# BSM 偏微分方程: Theta + (r-q) S Delta + 0.5 sigma^2 S^2 Gamma = r V
print("BSM PDE 残差最大值:", np.abs(th + (r-q)*S*d + 0.5*sig**2*S**2*g - r*px).max())

# 2) 向量化 IV vs 逐个 brentq
t0 = time.perf_counter(); iv, ok = implied_vol(px, S, K, T, r, q, cp); t1 = time.perf_counter()
m = 2000
t2 = time.perf_counter()
iv_b = [brentq(lambda s: bs_price(S, K[i], T[i], r, q, s, cp[i]) - px[i], 1e-4, 5.0, xtol=1e-12)
        if ok[i] else np.nan for i in range(m)]
t3 = time.perf_counter()
print(f"向量化 IV: {n} 个用时 {t1-t0:.2f}s;brentq 循环: {m} 个用时 {t3-t2:.2f}s"
      f"(折合每个 {1e6*(t1-t0)/n:.1f}us vs {1e6*(t3-t2)/m:.0f}us)")
print(f"可解比例 {ok.mean():.4f};可解样本 IV 最大误差 {np.nanmax(np.abs(iv-sig)):.2e}")

# 3) 报价精度对 IV 的影响:价格四舍五入到 0.01 后,深度虚值期权的 IV 误差
px_tick = np.round(px, 2)
iv_t, ok_t = implied_vol(px_tick, S, K, T, r, q, cp)
err = np.abs(iv_t - sig)
vega_pts = v / 100   # 1 个波动率点对应的价格变化
for lo_, hi_ in [(0, 0.005), (0.005, 0.02), (0.02, 0.1), (0.1, 1e9)]:
    sel = ok_t & (vega_pts >= lo_) & (vega_pts < hi_)
    print(f"vega/点 ∈ [{lo_},{hi_}): 样本 {sel.sum():6d}, IV 误差中位数 {np.median(err[sel])*100:.3f} 点, "
          f"99% 分位 {np.quantile(err[sel],0.99)*100:.2f} 点")
print("价格四舍五入后被判为不可解的比例:", round(1 - ok_t.mean(), 4))

输出:

put-call parity 最大误差: 3.552713678800501e-14
delta/gamma/vega 有限差分最大误差: 3.4e-08 1.4e-07 1.7e-07
BSM PDE 残差最大值: 4.4977910285126654e-14
向量化 IV: 200000 个用时 0.83s;brentq 循环: 2000 个用时 0.54s(折合每个 4.1us vs 271us)
可解比例 0.9801;可解样本 IV 最大误差 2.81e-10
vega/点 ∈ [0,0.005): 样本   4881, IV 误差中位数 1.821 点, 99% 分位 60.74 点
vega/点 ∈ [0.005,0.02): 样本   5903, IV 误差中位数 0.210 点, 99% 分位 0.84 点
vega/点 ∈ [0.02,0.1): 样本  19529, IV 误差中位数 0.042 点, 99% 分位 0.19 点
vega/点 ∈ [0.1,1000000000.0): 样本 161277, IV 误差中位数 0.008 点, 99% 分位 0.04 点
价格四舍五入后被判为不可解的比例: 0.0421

几点解读。看涨看跌平价、有限差分和 PDE 残差都在机器精度附近,说明统一 \(\phi\) 写法下的公式没有符号错误。向量化求解每个期权约 4 微秒,比逐个调用 brentq 快 60 多倍;每天收盘要为全市场几万条期权报价重算 IV 和希腊字母时,这个差别决定了能否在分钟级完成。

更重要的是最后一段。价格只是四舍五入到 0.01,vega 小于每波动率点 0.005 元的期权,IV 误差中位数就接近 2 个波动率点,99% 分位达到 60 个点;vega 大于 0.1 的期权误差不到 0.01 个点。这就是为什么做曲面时要按 vega 或按价差的倒数加权,并设置 vega 下限。代码中被判为"不可解"的约 2%(精确价格)和 4%(舍入后)的样本,绝大多数是这类深度虚值短期期权。

与真实数据的差别:个股期权多为美式,提前行权使看跌期权价格高于欧式公式,直接用 BSM 反解会得到偏高的 IV(应先用二叉树或用平价构造的欧式等价价格,见第 08 册第 21a 章);离散股息要用"减去股息现值"的处理(第 08 册第 15b 章),用连续股息率近似会在除息日附近产生明显误差。

9.3 隐含波动率曲面:清洗、远期、参数化与无套利

9.3.1 从报价到曲面的流水线

第 08 册第 20 章讲了微笑和偏斜为什么存在,第 20.9 节给出了由微笑反推风险中性密度的 Breeden–Litzenberger 公式。从实务角度,构建一张可用的曲面通常分五步:

  1. 远期和折现因子。不要用"现货乘以无风险利率减股息"去算远期,因为股息和融券成本往往未知。看涨看跌平价 \(C-P=e^{-rT}(F-K)\) 对每个期限是一条直线,用平值附近成对的中间价回归 \(C-P\) 对 \(K\),斜率是 \(-e^{-rT}\),截距除以负斜率就是 \(F\)。这一步同时得到隐含的股息率加融券成本。
  2. 只用虚值一侧。\(K<F\) 用看跌,\(K>F\) 用看涨。实值期权的价格主要是内在价值,时间价值占比小,反解 IV 对价格噪声更敏感。
  3. 过滤与加权。剔除买价为零、价差过宽、vega 过小的报价;按价差倒数或 vega 加权。
  4. 逐期限参数化。在对数行权价 \(k=\ln(K/F)\) 上拟合总方差 \(w(k)=\sigma^2(k)T\)。常用 Gatheral 的 raw SVI:
    \[w(k)=a+b\left[\rho(k-m)+\sqrt{(k-m)^2+s^2}\right].\]
    五个参数分别控制水平、两翼斜率、偏斜方向、平移和平值处的曲率。它的两翼渐近线性,和 Lee 的矩公式要求的形状一致。
  5. 无套利检查与期限插值。日历套利要求同一个 \(k\) 上的总方差随 \(T\) 不减;蝶式套利要求风险中性密度非负,对 SVI 等价于 Durrleman 条件
    \[g(k)=\Big(1-\frac{kw'}{2w}\Big)^2-\frac{w'^2}{4}\Big(\frac1w+\frac14\Big)+\frac{w''}{2}\ \ge 0.\]
    期限之间的插值在总方差上做,而不是在波动率上做:总方差对 \(T\) 线性插值能保证日历单调,在波动率上插值则不能。

推导拆解:第 1 步的平价回归。看涨看跌平价 \(C-P=e^{-rT}(F-K)=e^{-rT}F-e^{-rT}K\)。把同一期限、不同行权价的 \(C-P\) 对 \(K\) 做回归,就是一条直线 \(C-P=a+bK\):斜率 \(b=-e^{-rT}\)(折现因子取负),截距 \(a=e^{-rT}F\)。于是折现因子 \(=-b\),远期 \(F=a/(-b)\),代码里写成 df_hat, F_hat = -b, -a / b。股息、融券费都已经包含在市场的 \(F\) 里,不需要单独知道,这正是这种做法的好处。只用平值附近的行权价,是因为那里的买卖价差最窄,\(C-P\) 最准。 白话解释:为什么在总方差 \(w=\sigma^2T\) 上工作,而不是在波动率 \(\sigma\) 上?总方差是"从现在到到期日累计的不确定性",它天然随期限增加:6 个月累计的不确定性不可能少于 3 个月,否则就能做日历套利(卖短买长)。在总方差上做线性插值,两个不减的端点之间的线段也不减,单调性自动保住;而波动率可以随期限上下变化,在它上面插值没有这样的保证。SVI 的五个参数可以这样记:\(a\) 抬高或压低整体水平,\(b\) 控制两翼张开的程度,\(\rho\) 让左右两翼不对称(股指为负,左翼更陡),\(m\) 左右平移,\(s\) 控制平值附近的圆滑程度。Durrleman 条件 \(g(k)\ge0\) 等价于"由这条微笑反推出的风险中性密度处处非负",负的密度意味着某个蝶式组合价格为负,即白拿钱的套利。

9.3.2 代码:SSVI 真值、平价回归、SVI 拟合与套利检查

为了能检验拟合质量,下面用一个满足无套利条件的 SSVI 曲面(Gatheral–Jacquier 2014)作为"真值",生成 5 个期限、每个期限约 40 个行权价的看涨和看跌报价,加入随价格变化的买卖价差、0.01 的最小报价单位和中间价噪声。研究者假装不知道股息率。

# 代码 9.2  隐含波动率曲面:平价回归求远期、SVI 拟合、无套利检查与期限插值
import numpy as np
from scipy.stats import norm
from scipy.optimize import least_squares, brentq

def black_price(F, K, T, df, sig, cp):            # 以远期价表示的 Black 公式
    s = sig * np.sqrt(T)
    d1 = np.log(F / K) / s + 0.5 * s; d2 = d1 - s
    return df * cp * (F * norm.cdf(cp * d1) - K * norm.cdf(cp * d2))

def black_iv(p, F, K, T, df, cp):
    f = lambda s: black_price(F, K, T, df, s, cp) - p
    try:
        return brentq(f, 1e-4, 4.0, xtol=1e-10)
    except ValueError:
        return np.nan

# ---------- 1) "真实"曲面:SSVI(Gatheral–Jacquier 2014),参数满足无套利条件 ----------
rho, eta, gam = -0.65, 1.2, 0.45
theta = lambda T: 0.03 * T + 0.012 * (1 - np.exp(-4 * T))      # 平值总方差,随 T 递增
phi = lambda th: eta / th**gam
def w_ssvi(k, T):
    th = theta(T); p = phi(th)
    return 0.5 * th * (1 + rho * p * k + np.sqrt((p * k + rho)**2 + 1 - rho**2))

S0, r, q_true = 100.0, 0.03, 0.018                              # 股息率 q 对研究者"未知"
mats = np.array([21, 42, 63, 126, 252]) / 252
rng = np.random.default_rng(11)
quotes = []
for T in mats:
    F_true, df = S0 * np.exp((r - q_true) * T), np.exp(-r * T)
    sd = np.sqrt(theta(T))
    Ks = np.round(F_true * np.exp(np.linspace(-3.5, 2.0, 41) * sd), 1)
    for K in np.unique(Ks):
        k = np.log(K / F_true)
        sig = np.sqrt(w_ssvi(k, T) / T)
        for cp in (1, -1):
            mid = black_price(F_true, K, T, df, sig, cp)
            half = max(0.01, 0.03 * mid) / 2 + 0.005               # 价差:最少 1 个 tick
            noisy = mid + rng.normal(0, 0.25 * half)
            bid, ask = np.floor((noisy - half) * 100) / 100, np.ceil((noisy + half) * 100) / 100
            quotes.append((T, K, cp, max(bid, 0.0), ask, sig))
Q = np.array(quotes)
print("报价条数:", len(Q), ";bid=0 的条数:", int((Q[:, 3] == 0).sum()))

# ---------- 2) 由看涨-看跌平价回归同时估计折现因子与远期:C-P = df*F - df*K ----------
fits = {}
for T in mats:
    sl = Q[Q[:, 0] == T]
    Ks = np.unique(sl[:, 1])
    C = np.array([sl[(sl[:, 1] == K) & (sl[:, 2] == 1)][0, 3:5].mean() for K in Ks])
    P = np.array([sl[(sl[:, 1] == K) & (sl[:, 2] == -1)][0, 3:5].mean() for K in Ks])
    F_guess = Ks[np.argmin(np.abs(C - P))]
    near = np.abs(np.log(Ks / F_guess)) < 0.1                  # 只用平值附近、价差窄的执行价
    b, a = np.polyfit(Ks[near], (C - P)[near], 1)              # C-P = a + b K, a=df*F, b=-df
    df_hat, F_hat = -b, -a / b
    # 只用虚值一侧:K<F 用看跌,K>F 用看涨;剔除 bid=0
    ks, ivs, wts = [], [], []
    for K in Ks:
        cp = -1 if K < F_hat else 1
        row = sl[(sl[:, 1] == K) & (sl[:, 2] == cp)][0]
        if row[3] <= 0: continue
        iv = black_iv(row[3:5].mean(), F_hat, K, T, df_hat, cp)
        if not np.isfinite(iv): continue
        ks.append(np.log(K / F_hat)); ivs.append(iv)
        wts.append(1 / (row[4] - row[3]))                       # 价差越窄,权重越大
    fits[T] = dict(F=F_hat, df=df_hat, k=np.array(ks), iv=np.array(ivs), w=np.array(wts))
    print(f"T={T*252:4.0f}天  F_hat={F_hat:8.4f} (真值 {S0*np.exp((r-q_true)*T):8.4f})  "
          f"隐含 q={r - np.log(F_hat/S0)/T:.4f}  可用虚值点 {len(ks)}")

# ---------- 3) 每个期限拟合 raw SVI:w(k)=a+b[rho(k-m)+sqrt((k-m)^2+s^2)] ----------
def svi(p, k):
    a, b, rh, m, s = p
    return a + b * (rh * (k - m) + np.sqrt((k - m)**2 + s**2))
def fit_svi(k, w, wt):
    x0 = [w.min() * 0.5, 0.1, -0.5, 0.0, 0.1]
    lb, ub = [-1, 1e-4, -0.999, -1, 1e-3], [1, 5, 0.999, 1, 2]
    res = least_squares(lambda p: wt * (svi(p, k) - w), x0, bounds=(lb, ub))
    return res.x
params = {}
for T, d in fits.items():
    w_mkt = d["iv"]**2 * T
    params[T] = fit_svi(d["k"], w_mkt, d["w"] / d["w"].max())
    iv_fit = np.sqrt(svi(params[T], d["k"]) / T)
    iv_true = np.sqrt(w_ssvi(d["k"] + np.log(d["F"] / (S0*np.exp((r-q_true)*T))), T) / T)
    print(f"T={T*252:4.0f}天  拟合 vs 市场 RMSE {np.sqrt(np.mean((iv_fit-d['iv'])**2))*100:.3f} 点;"
          f"拟合 vs 真值 RMSE {np.sqrt(np.mean((iv_fit-iv_true)**2))*100:.3f} 点")

# ---------- 4) 无套利检查:日历(总方差随 T 不减)与蝶式(Durrleman 条件 g(k)>=0) ----------
kg = np.linspace(-0.6, 0.4, 401)
W = np.array([svi(params[T], kg) for T in mats])
print("日历套利违例(相邻期限总方差下降的格点数):", int((np.diff(W, axis=0) < -1e-8).sum()))
def durrleman_g(p, k, h=1e-4):
    w = svi(p, k); w1 = (svi(p, k+h) - svi(p, k-h)) / (2*h)
    w2 = (svi(p, k+h) - 2*w + svi(p, k-h)) / h**2
    return (1 - k * w1 / (2 * w))**2 - w1**2 / 4 * (1 / w + 0.25) + w2 / 2
for T in mats:
    g = durrleman_g(params[T], kg)
    print(f"T={T*252:4.0f}天  min g(k) = {g.min():+.4f}  {'通过' if g.min() >= 0 else '存在蝶式套利'}")

# ---------- 5) 期限插值:在"总方差"上沿 T 线性插值,得到 45 天的 25 delta 偏斜 ----------
T45 = 45 / 252
i = np.searchsorted(mats, T45); T1, T2 = mats[i-1], mats[i]
wgt = (T2 - T45) / (T2 - T1)
w45 = lambda k: wgt * svi(params[T1], k) + (1 - wgt) * svi(params[T2], k)
for k in (-0.10, 0.0, 0.05):
    print(f"45 天, k={k:+.2f}: 插值 IV {np.sqrt(w45(k)/T45)*100:.2f}%  真值 {np.sqrt(w_ssvi(k,T45)/T45)*100:.2f}%")

输出:

报价条数: 410 ;bid=0 的条数: 5
T=  21天  F_hat=100.1046 (真值 100.1001)  隐含 q=0.0175  可用虚值点 39
T=  42天  F_hat=100.2055 (真值 100.2002)  隐含 q=0.0177  可用虚值点 40
T=  63天  F_hat=100.3179 (真值 100.3005)  隐含 q=0.0173  可用虚值点 40
T= 126天  F_hat=100.5748 (真值 100.6018)  隐含 q=0.0185  可用虚值点 40
T= 252天  F_hat=101.2238 (真值 101.2072)  隐含 q=0.0178  可用虚值点 41
T=  21天  拟合 vs 市场 RMSE 0.093 点;拟合 vs 真值 RMSE 0.024 点
T=  42天  拟合 vs 市场 RMSE 0.091 点;拟合 vs 真值 RMSE 0.068 点
T=  63天  拟合 vs 市场 RMSE 0.089 点;拟合 vs 真值 RMSE 0.062 点
T= 126天  拟合 vs 市场 RMSE 0.062 点;拟合 vs 真值 RMSE 0.050 点
T= 252天  拟合 vs 市场 RMSE 0.046 点;拟合 vs 真值 RMSE 0.034 点
日历套利违例(相邻期限总方差下降的格点数): 0
T=  21天  min g(k) = +0.2954  通过
T=  42天  min g(k) = +0.3100  通过
T=  63天  min g(k) = +0.3181  通过
T= 126天  min g(k) = +0.3288  通过
T= 252天  min g(k) = +0.3473  通过
45 天, k=-0.10: 插值 IV 32.56%  真值 32.60%
45 天, k=+0.00: 插值 IV 25.31%  真值 25.36%
45 天, k=+0.05: 插值 IV 21.84%  真值 21.93%

平价回归估出的远期与真值的差在 0.03 以内,隐含股息率在 1.73% 到 1.85% 之间(真值 1.8%),说明不需要知道股息也能把远期估准。SVI 拟合到市场中间价的 RMSE 不到 0.1 个波动率点,拟合到真值的误差更小,说明拟合主要在平滑报价噪声而没有过度追随噪声。日历和蝶式检查全部通过,45 天的插值与真值相差不到 0.1 个点。

这个例子"太顺利"了,因为真值本身就来自 SSVI 族,SVI 能完美表达。真实数据中常见的问题是:短期限的微笑在平值附近有尖峰(事件前),SVI 拟合不好;两翼报价稀疏时,自由的 \(b\) 和 \(\rho\) 会让外推出现蝶式违例;不同期限分别拟合后,在深度虚值处出现日历违例。实务做法是先拟合 SSVI 这类带全局无套利约束的曲面,再在每个期限上做小幅修正,或在优化中把 \(g(k)\ge0\) 和日历条件作为约束。练习 2 会让你制造并修复这类违例。

9.3.3 从曲面提取的常用信号

拿到干净的曲面后,常用的信号有:平值 IV 的水平与期限斜率(短期高于长期往往对应恐慌期);25 delta 风险逆转(看跌 IV 减看涨 IV)度量偏斜;用第 08 册第 26b 章式 (26.6) 由整条微笑计算的模型无关方差(VIX 的做法),以及它与已实现方差之差,也就是下一节的方差风险溢价。注意这些信号都要在固定的"常数期限"上比较,例如 30 天,否则到期日临近本身就会让平值 IV 发生机械变化。

9.4 方差风险溢价:卖保险的收益

9.4.1 定义与经验事实

记 \(\hat E\) 为风险中性期望,\(E^P\) 为现实概率下的期望,\(\bar V\) 为未来一段时间的年化已实现方差。方差风险溢价(第 08 册第 26b 章)定义为

\[\text{VRP}=\hat E(\bar V)-E^P(\bar V).\]

\(\hat E(\bar V)\) 就是方差互换的公平执行价,可以由期权价格静态复制;\(E^P(\bar V)\) 需要用模型(GARCH、HAR 等,见第 06 册第 03a、03b 章)预测。股指上 VRP 长期为正,这意味着隐含方差系统性高估了随后的实现方差,卖出方差(卖方差互换,或卖出 delta 对冲的跨式组合)平均赚钱。

卖方差的收益结构和卖保险一样:大多数月份小赚,偶尔一个月巨亏。因此用 Sharpe 评价它会严重误导。第 07 册第 22b 章的"比索问题"讨论的正是这种情况:样本期没有出现灾难,不代表策略没有灾难风险。

金融直觉:把 VRP 想成财险公司的综合成本率。保费(隐含方差)里除了预期赔付(现实期望方差),还包含承担巨灾风险的利润和资本成本。股指期权的买方多是要给组合买保险的机构,愿意付这笔溢价;卖方(做市商、波动率基金)收下溢价,同时承担"所有保单同时出险"的风险:市场崩盘时实现方差暴涨,所有卖方同时巨亏。所以 VRP 为正并不说明市场定价错了,它是对一种很难分散的风险的合理报酬。期权做市商的风险管理本质上就是保险公司的再保险和巨灾准备金管理。

9.4.2 代码:合成市场中的 VRP 与卖方差策略

模拟 30 年日收益:GJR-GARCH(1,1),标准化 t5 新息,平均每 500 天一次均值 −6% 的跳跃。每月初的"隐含方差"设为模型对未来 21 天方差的条件期望乘以 1.35。这是一个已知 VRP 的世界,可以检验常用的分析工具能否识别它,以及卖方差的收益分布长什么样。

# 代码 9.3  方差风险溢价与卖出方差互换
import numpy as np
import pandas as pd
import statsmodels.api as sm

# ---------- 1) 合成指数:GJR-GARCH(1,1) + 标准化 t5 新息 + 稀有负跳跃 ----------
rng = np.random.default_rng(2024)
n_days, H = 252 * 30, 21
omega, alpha, gamma_, beta = 1.5e-6, 0.02, 0.12, 0.90           # 持续性 alpha+gamma/2+beta = 0.98
nu = 5
lam, mu_j, sd_j = 1 / 500, -0.06, 0.03                           # 平均每两年一次跳跃
z = rng.standard_t(nu, n_days) / np.sqrt(nu / (nu - 2))
jumps = (rng.random(n_days) < lam) * rng.normal(mu_j, sd_j, n_days)
h = np.empty(n_days); ret = np.empty(n_days)
h[0] = omega / (1 - alpha - gamma_ / 2 - beta)
for t in range(n_days):
    if t > 0:
        e = ret[t-1] - jumps[t-1]
        h[t] = omega + (alpha + gamma_ * (e < 0)) * e**2 + beta * h[t-1]
    ret[t] = 0.0003 + np.sqrt(h[t]) * z[t] + jumps[t]

# ---------- 2) 每月初的"隐含方差":模型对未来 21 天方差的条件期望 × 风险溢价 ----------
pers = alpha + gamma_ / 2 + beta; h_bar = omega / (1 - pers)
def expected_var(h_next):       # sum_{i=1..H} E[h_{t+i}] + 跳跃方差
    i = np.arange(H)
    return np.sum(h_bar + pers**i * (h_next - h_bar)) + H * lam * (mu_j**2 + sd_j**2)
premium = 1.35                   # 隐含方差 = 1.35 × 真实期望方差(隐含波动率约高 16%)
starts = np.arange(0, n_days - H, H)
rows = []
for s in starts:
    EV = expected_var(h[s])
    iv2 = premium * EV * 252 / H                                  # 年化隐含方差
    rv2 = np.sum(ret[s:s+H]**2) * 252 / H                         # 年化实现方差
    rows.append((s, np.sqrt(iv2), np.sqrt(rv2), iv2, rv2))
df = pd.DataFrame(rows, columns=["t", "iv", "rv", "iv2", "rv2"])
print("平均隐含波动率 %.1f%%,平均实现波动率 %.1f%%,IV>RV 的月份比例 %.0f%%"
      % (100*df.iv.mean(), 100*df.rv.mean(), 100*(df.iv > df.rv).mean()))

# ---------- 3) 无偏性回归:RV^2 = a + b*IV^2;风险溢价下 b<1 ----------
X = sm.add_constant(df.iv2); m = sm.OLS(df.rv2, X).fit(cov_type="HAC", cov_kwds={"maxlags": 3})
print("RV² 对 IV² 回归: a=%.4f  b=%.3f (HAC t[b=1]=%.2f)" % (m.params.iloc[0], m.params.iloc[1],
      (m.params.iloc[1]-1)/m.bse.iloc[1]))

# ---------- 4) 卖出方差互换:vega 名义 = 资本的 0.2%/波动率点,每月结算 ----------
# 收益 = N_var*(K_var - RV),波动率以"点"(%)计,N_var = N_vega/(2*K_vol)
vega_notional = 0.002
N_var = vega_notional / (2 * df.iv * 100)
pnl = N_var * ((df.iv * 100)**2 - (df.rv * 100)**2)
r_eq = np.array([np.sum(ret[s:s+H]) for s in starts])
def stats(x, name):
    x = np.asarray(x); eq = np.cumsum(x); dd = (eq - np.maximum.accumulate(eq)).min()
    print(f"{name:10s} 月均 {100*x.mean():6.2f}%  年化 Sharpe {x.mean()/x.std()*np.sqrt(12):5.2f}  "
          f"偏度 {pd.Series(x).skew():6.2f}  最差月 {100*x.min():7.2f}%  最大回撤 {100*dd:7.2f}%")
stats(pnl, "卖方差"); stats(r_eq, "持有指数")

# ---------- 5) 短样本错觉:滚动 5 年 Sharpe 的分布 ----------
sr5 = pd.Series(pnl).rolling(60).apply(lambda x: x.mean()/x.std()*np.sqrt(12)).dropna()
print("滚动 5 年 Sharpe:中位数 %.2f,最高 %.2f,最低 %.2f" % (sr5.median(), sr5.max(), sr5.min()))
worst = np.argsort(pnl.values)[:3]
print("亏损最大的 3 个月:", [f"第{i}月 {100*pnl.iloc[i]:.1f}% (IV {100*df.iv[i]:.0f}%, RV {100*df.rv[i]:.0f}%)" for i in worst])
print("最差 3 个月合计占全部累计盈利的比例: %.0f%%" % (-100 * pnl.iloc[worst].sum() / pnl.sum()))

输出:

平均隐含波动率 14.4%,平均实现波动率 10.7%,IV>RV 的月份比例 89%
RV² 对 IV² 回归: a=0.0024  b=0.554 (HAC t[b=1]=-5.73)
卖方差        月均   0.47%  年化 Sharpe  1.15  偏度  -6.45  最差月  -13.91%  最大回撤  -14.29%
持有指数       月均   0.45%  年化 Sharpe  0.44  偏度  -1.17  最差月  -16.62%  最大回撤  -53.62%
滚动 5 年 Sharpe:中位数 1.67,最高 5.44,最低 0.32
亏损最大的 3 个月: ['第105月 -13.9% (IV 12%, RV 42%)', '第197月 -12.9% (IV 16%, RV 48%)', '第27月 -6.7% (IV 13%, RV 32%)']
最差 3 个月合计占全部累计盈利的比例: 20%

推导拆解:代码 9.3 中两个公式需要补上来历。 其一,expected_var:GJR-GARCH 的多步预测。持续性 \(p=\alpha+\gamma/2+\beta\)(\(\gamma/2\) 是因为负冲击大约占一半),长期方差 \(\bar h=\omega/(1-p)\)。对 \(\mathbb E[h_{t+1}]=\omega+p\,h_t\) 反复迭代,可得 \(\mathbb E[h_{t+i}]-\bar h=p^{\,i}(h_t-\bar h)\),即当前方差对长期水平的偏离按 \(p^{\,i}\) 几何衰减。把未来 21 天加起来,再加上跳跃贡献的方差(每天 \(\lambda\) 的概率发生跳跃,跳跃平方的期望是 \(\mu_j^2+s_j^2\)),就是未来一个月的期望方差。这里用到了几何级数的思想,见 第 00 册第 04 章 级数与收敛。 其二,方差互换的名义金额换算 \(N_{\text{var}}=N_{\text{vega}}/(2K_{\text{vol}})\):方差互换按 \(\sigma^2\) 结算,交易员却习惯按"每个波动率点赚多少钱"(vega 名义)下单。由一阶近似 \(d(\sigma^2)\approx2\sigma\,d\sigma\)(\(\sigma^2\) 对 \(\sigma\) 求导),实现波动率在执行价附近每变动 1 点,方差变动约 \(2K_{\text{vol}}\) 点,所以方差名义要除以 \(2K_{\text{vol}}\),才能让两者在执行价附近的敏感度相同。偏离执行价越远,这个线性近似越不准:方差互换对实现波动率是凸的,上涨时亏得比线性近似更多,这正是卖方偏度极负的来源之一。

结果说明了四件事。

第一,隐含波动率在 89% 的月份高于实现波动率,平均高 3.7 个点。这与真实股指期权的经验相近,但真实市场的 VRP 本身随时间剧烈变化,危机后往往更高。

第二,"RV² 对 IV² 回归"斜率 0.55,显著小于 1(HAC 标准误,第 05 册第 16 章、第 06 册第 02b 章)。文献中常用这个回归检验隐含波动率是否为无偏预测。斜率小于 1 有两个来源:风险溢价本身(本例中斜率的理论值约为 \(1/1.35\approx0.74\))和实现方差中厚尾带来的巨大噪声。

第三,卖方差的年化 Sharpe 为 1.15,是持有指数的两倍多,但偏度为 −6.5,最差一个月亏损 13.9%,最大回撤几乎全由这一个月造成。最差 3 个月吞掉了 30 年累计盈利的 20%。

第四,滚动 5 年 Sharpe 中位数 1.67,最高 5.4。如果研究者恰好拿到一段没有跳跃的 5 年样本,会得到一个极其诱人的数字。这就是为什么评价这类策略时,要报告偏度、最差月、ES 和压力情景(本册第 11 章),而不是只报告 Sharpe;也是为什么规模控制应该基于"最坏情景下亏多少",而不是基于历史波动率。

与真实数据的差别:本例的 VRP 是常数倍数,真实 VRP 随市场状态变化,并且在崩盘后隐含波动率会跳升,卖方在按市值计价时会先承受 vega 损失,再承受实现方差损失;真实方差互换和期权组合还有买卖价差、保证金追加和流动性枯竭,这些都会把模拟里的 Sharpe 再打折扣。

9.5 Delta 对冲盈亏分解:gamma 与 theta

9.5.1 公式

第 08 册第 19b 章推出:买入一份期权、按定价波动率 \(\sigma_i\) 做 delta 对冲、其余资金按无风险利率借贷,在一个对冲区间内的盈亏约为

\[\text{P\&L}_t\approx\tfrac12\Gamma_tS_t^2\left[R_t^2-\sigma_i^2\Delta t\right],\qquad R_t=\frac{\Delta S_t}{S_t}.\]

整段持有期把它折现加总,得到

\[\text{P\&L}\approx\sum_t e^{-rt}\,\tfrac12\Gamma_tS_t^2\left(R_t^2-\sigma_i^2\Delta t\right).\]

这就是"gamma–theta"分解:gamma 让你从每一次价格波动 \(R_t^2\) 中赚钱,theta 让你每天支付 \(\tfrac12\Gamma S^2\sigma_i^2\Delta t\)。两者的平衡点就是实现方差等于隐含方差。它也说明了 delta 对冲期权和方差互换的差别:期权的权重是 dollar gamma \(\tfrac12\Gamma S^2\),随标的价格和剩余期限变化,所以同样的实现方差,发生在平值附近时比发生在深度虚值时赚得多;方差互换的权重是常数,消除了这种路径依赖。

推导拆解:gamma–theta 公式只需要泰勒展开加 BSM 方程(为简洁取 \(q=0\))。 第一步,组合:买 1 份期权,卖空 \(\Delta\) 股,剩余资金 \(-(V-\Delta S)\) 按 \(r\) 借贷。一个小区间 \(\Delta t\) 内组合的盈亏是 \(\Delta V-\Delta\cdot\Delta S-r(V-\Delta S)\Delta t\)。 第二步,对期权价格做二阶泰勒展开(对 \(S\) 展开到二阶,对 \(t\) 展开到一阶):\(\Delta V\approx\Delta\cdot\Delta S+\tfrac12\Gamma(\Delta S)^2+\Theta\Delta t\)。代入后 \(\Delta\cdot\Delta S\) 一项正好被对冲抵消,剩下 \(\tfrac12\Gamma(\Delta S)^2+\Theta\Delta t-r(V-\Delta S)\Delta t\)。 第三步,用 BSM 方程(按定价波动率 \(\sigma_i\))替换 \(\Theta\):\(\Theta=rV-rS\Delta-\tfrac12\sigma_i^2S^2\Gamma\)。代入后利息项全部抵消,只剩 \(\tfrac12\Gamma(\Delta S)^2-\tfrac12\Gamma\sigma_i^2S^2\Delta t\)。 第四步,把 \((\Delta S)^2\) 写成 \(S^2R^2\),提出 \(\tfrac12\Gamma S^2\),就得到正文公式。 这和 CFA 固定收益里"久期中性的组合靠凸性赚钱"是同一回事:久期对冲掉一阶风险后,正凸性让你在收益率大幅波动时获利,代价是正凸性债券的收益率通常更低(相当于 theta)。期权做市商每天的账本就是这个公式:卖出期权(负 gamma)收 theta,市场平静时 \(R^2<\sigma_i^2\Delta t\),赚钱;市场剧烈波动时 \(R^2\) 远大于 \(\sigma_i^2\Delta t\),亏钱,而且亏损按 dollar gamma 加权,平值附近的大波动最伤。

实务中这个分解有三个用途:把一个期权账户的日盈亏拆成 delta、gamma–theta、vega 和残差,用于风控和交易员考核;检验对冲引擎是否正确(残差应当很小);研究对冲频率和交易成本的权衡。

9.5.2 代码:分解的验证、对冲波动率选择与对冲频率

模拟 63 个交易日的路径,实现波动率本身随机(对数 OU 过程,围绕设定水平波动)。买入一份 3 个月平值看涨,按隐含波动率 20% 成交。

# 代码 9.4  Delta 对冲盈亏的 gamma–theta 分解、对冲波动率与对冲频率
import numpy as np
from scipy.stats import norm

def bs_call(S, K, T, r, sig):
    T = np.maximum(T, 1e-12); s = sig * np.sqrt(T)
    d1 = (np.log(S / K) + (r + 0.5 * sig**2) * T) / s
    return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d1 - s), norm.cdf(d1), norm.pdf(d1) / (S * s)

def simulate(n_paths, steps_per_day, rv_level, seed, days=63, S0=100.0, r=0.03):
    """实现波动率围绕 rv_level 随机变动(对数 OU),每日 steps_per_day 个对冲点"""
    rng = np.random.default_rng(seed)
    n = days * steps_per_day; dt = 1 / 252 / steps_per_day
    lv = np.full(n_paths, np.log(rv_level)); S = np.empty((n_paths, n + 1)); S[:, 0] = S0
    vol = np.empty((n_paths, n))
    for i in range(n):
        lv += -2.0 * (lv - np.log(rv_level)) * dt + 0.8 * np.sqrt(dt) * rng.standard_normal(n_paths)
        vol[:, i] = np.exp(lv)
        S[:, i+1] = S[:, i] * np.exp((r - 0.5 * vol[:, i]**2) * dt + vol[:, i] * np.sqrt(dt) * rng.standard_normal(n_paths))
    return S, vol, dt

def hedge(S, dt, sig_i, sig_h, r=0.03, K=100.0, cost=0.0):
    """买入 1 份平值看涨(按隐含 sig_i 成交),用 sig_h 计算 delta 离散对冲;现金按 r 计息。
    返回总 P&L、gamma-theta 项之和、交易成本"""
    n = S.shape[1] - 1; T0 = n * dt
    _, d_prev, _ = bs_call(S[:, 0], K, T0, r, sig_h)
    price0, _, _ = bs_call(S[:, 0], K, T0, r, sig_i)
    cash = -price0 + d_prev * S[:, 0] - cost * np.abs(d_prev) * S[:, 0]
    tc = cost * np.abs(d_prev) * S[:, 0]; gt = np.zeros(S.shape[0])
    for i in range(1, n + 1):
        tau_prev = T0 - (i - 1) * dt
        _, _, g_prev = bs_call(S[:, i-1], K, tau_prev, r, sig_i)
        R = S[:, i] / S[:, i-1] - 1
        gt += 0.5 * g_prev * S[:, i-1]**2 * (R**2 - sig_i**2 * dt) * np.exp(-r * (i - 1) * dt)
        cash *= np.exp(r * dt)
        if i < n:
            _, d_new, _ = bs_call(S[:, i], K, T0 - i * dt, r, sig_h)
            trade = d_new - d_prev
            cash += trade * S[:, i]; c_ = cost * np.abs(trade) * S[:, i]; cash -= c_; tc += c_
            d_prev = d_new
    payoff = np.maximum(S[:, -1] - K, 0)
    pnl = (payoff - d_prev * S[:, -1] + cash) * np.exp(-r * T0)    # 折回 0 时刻
    return pnl, gt, tc * 1.0

sig_i = 0.20
# 1) 实现波动率 = 隐含波动率、高于、低于:P&L 的均值与 gamma-theta 项的解释力
for rv in (0.15, 0.20, 0.25):
    S, vol, dt = simulate(4000, 1, rv, seed=1)
    pnl, gt, _ = hedge(S, dt, sig_i, sig_i)
    print(f"实现波动率≈{rv:.0%}: 平均 P&L {pnl.mean():6.3f}  标准差 {pnl.std():.3f}  "
          f"gamma-theta 项均值 {gt.mean():6.3f}  corr(P&L, gamma-theta) {np.corrcoef(pnl, gt)[0,1]:.3f}")

# 2) 用隐含波动率还是实现波动率算 delta:均值相同,路径分布不同
S, vol, dt = simulate(4000, 1, 0.25, seed=2)
for name, sh in (("用隐含 20% 对冲", 0.20), ("用实现 25% 对冲", 0.25)):
    pnl, _, _ = hedge(S, dt, sig_i, sh)
    print(f"{name}: 平均 P&L {pnl.mean():.3f}  标准差 {pnl.std():.3f}  P(P&L<0) {np.mean(pnl<0):.2f}")

# 3) 对冲频率与交易成本:单边 5bp
print("对冲频率    无成本均值  无成本标准差   含成本均值   平均成本")
for spd, label in ((1, "每日 1 次"), (4, "每日 4 次"), (16, "每日 16 次")):
    S, vol, dt = simulate(3000, spd, 0.20, seed=3)
    p0, _, _ = hedge(S, dt, sig_i, sig_i, cost=0.0)
    p1, _, tc = hedge(S, dt, sig_i, sig_i, cost=0.0005)
    print(f"{label:8s}   {p0.mean():8.3f}   {p0.std():8.3f}    {p1.mean():8.3f}   {tc.mean():6.3f}")
c, _, _ = bs_call(100.0, 100.0, 63/252, 0.03, 0.2)
print(f"参照:期权价格 {c:.3f},1 个波动率点的 vega ≈ {(bs_call(100.,100.,63/252,0.03,0.21)[0]-c):.3f}")

输出:

实现波动率≈15%: 平均 P&L -0.883  标准差 0.817  gamma-theta 项均值 -0.879  corr(P&L, gamma-theta) 0.995
实现波动率≈20%: 平均 P&L  0.142  标准差 1.065  gamma-theta 项均值  0.144  corr(P&L, gamma-theta) 0.993
实现波动率≈25%: 平均 P&L  1.164  标准差 1.444  gamma-theta 项均值  1.171  corr(P&L, gamma-theta) 0.988
用隐含 20% 对冲: 平均 P&L 1.188  标准差 1.518  P(P&L<0) 0.14
用实现 25% 对冲: 平均 P&L 1.188  标准差 1.378  P(P&L<0) 0.14
对冲频率    无成本均值  无成本标准差   含成本均值   平均成本
每日 1 次        0.189      1.056       0.037    0.153
每日 4 次        0.201      1.014      -0.080    0.282
每日 16 次       0.158      0.951      -0.375    0.535
参照:期权价格 4.358,1 个波动率点的 vega ≈ 0.198

第一段:gamma–theta 项与总盈亏的相关系数在 0.99 左右,均值几乎相同,说明公式抓住了主要来源,剩下的是三阶项和离散化残差。实现波动率围绕 15%、20%、25% 波动时,平均盈亏分别为 −0.88、+0.14、+1.16,大约是每个波动率点 0.2 元,与 vega(约 0.198)一致。注意"实现波动率约 20%"时平均盈亏并不为零:对数 OU 过程下波动率的均值略高于 20%,再加上方差对波动率的凸性,实现方差的期望高于 \(0.2^2\)。这提醒我们,比较隐含和实现时应当比方差,而不是比波动率。

第二段:用实现波动率 25% 而不是隐含波动率 20% 计算 delta,平均盈亏不变,但标准差从 1.52 降到 1.38。用隐含波动率对冲时,盈亏是一个依赖路径的 gamma 加权积分;用真实波动率对冲时,盈亏在到期时锁定为两个价格之差,路径不确定性更小,但中途的按市值计价盈亏波动更大。实务中真实波动率未知,通常用隐含波动率对冲,或者两者之间取一个预测值。

第三段:把对冲频率从每天 1 次提高到 16 次,无成本时盈亏标准差只从 1.06 降到 0.95。原因是这里的主要不确定性来自实现波动率本身的随机性,而不是离散对冲误差;更频繁的对冲只能减少后者。与此同时,单边 5bp 的成本从 0.15 增加到 0.54,相当于 2.7 个波动率点的 vega。Leland(1985)把这种权衡写成对波动率的修正:频繁对冲的成本等价于把买方的有效波动率调高。实务中常用"delta 偏离超过阈值才对冲"的带宽规则,而不是固定时间间隔。

白话解释:代码 9.4 的 hedge 函数就是一本逐笔记账的对冲账户,和第 06 章的事件驱动回测是同一种写法。开始时付出期权费(按 \(\sigma_i\) 定价)、卖出 \(\Delta\) 股,现金记为 cash;之后每个对冲点,现金先按无风险利率计息,再按新 delta 调整股票持仓,买卖差额进出现金,交易成本单独累加到 tc;到期时收到期权收益、平掉股票空头,所有现金折回 0 时刻就是总盈亏。与此同时,gt 按上面推导的公式逐期累加 \(\tfrac12\Gamma S^2(R^2-\sigma_i^2\Delta t)\) 并折现。两者相关 0.99,就是"公式解释了盈亏"的数值证据;做市台用同样的方法给每个交易员的日盈亏做归因,残差突然变大时先怀疑系统出错。 注意函数有两个波动率参数:sig_i 决定期权费和 gamma–theta 项里的 gamma,sig_h 决定实际对冲用的 delta。把两者分开,才能做第二段"用隐含还是用实现波动率对冲"的比较。

与真实数据的差别:真实标的会跳空,跳空时 gamma–theta 项一次性贡献巨大,离散对冲无法避免;隐含波动率每天都在变,需要在分解中加入 \(\text{Vega}\cdot\Delta\sigma_i\) 项和 vanna、volga 等交叉项;个股期权还有借券成本和提前行权。

9.6 期货展期、基差与 carry

9.6.1 连续合约

期货有到期日,研究时要把一串合约拼成连续序列。持有成本模型(第 08 册第 05 章)给出

\[F(t,T)=S_t\,e^{(r+u-y)(T-t)},\]

\(u\) 为存储成本率,\(y\) 为便利收益率。\(y>r+u\) 时期货曲线向下倾斜(贴水,backwardation),近月高于远月;反之为升水(contango)。基差 \(S_t-F(t,T)\) 在到期时收敛到零(第 08 册第 02、03 章)。

最常见的错误是把每天的近月价格直接首尾相连。换月那一天,序列从旧合约跳到新合约,跳空幅度等于两个合约的价差 \((r+u-y)(T_2-T_1)\)。这个跳空不是任何人能赚到或亏掉的收益。正确做法是:

  • 收益序列:每天的收益只用同一个合约前后两天的价格计算,换月当天用旧合约收盘到旧合约收盘,第二天起用新合约。这是回测唯一应当使用的序列。
  • 后复权价格序列:从最近一次换月往前,把每次跳空从历史价格中扣掉(价差复权)或按比例缩放(比例复权)。价差复权保留价格差,适合计算以"点"为单位的指标;比例复权保留收益率,但历史价格可能变成负数以外的奇怪数值。技术指标、均线等用后复权序列计算。

白话解释:上一条最后一句读起来有些拗口,这里澄清一下两种复权的区别(这是对原文措辞的补充说明)。价差复权是把历史价格整体加减一个常数,长期贴水或升水累积后,久远的历史价格可能被减成负数,这时收益率(价格比值)就失去意义;比例复权是把历史价格整体乘一个系数,价格永远为正,日收益率与真实持仓收益一致,但价格水平不再等于当时的真实成交价,所以"价格突破某个整数关口"之类的水平类信号会失真。代码 9.5 对对数价格做价差复权,在对数尺度上相减等价于在原价格上按比例缩放,所以属于比例复权。

  • 换月规则:在到期前若干天、或在持仓量与成交量转移到次月时换月,避免进入交割月的流动性下降和交割风险。

9.6.2 展期收益不等于预期收益

把持有期货的收益拆开:

\[d\ln F(t,T)=d\ln S_t-(r+u-y_t)\,dt-(T-t)\,dy_t.\]

忽略最后一项,期货超额收益约等于现货价格收益加上"展期收益" \((y-r-u)\,dt\)。贴水市场的展期收益为正,于是常见的说法是"做多贴水品种能赚展期收益"。这个说法只对了一半。如果期货价格是未来现货的无偏预测,那么贴水本身就意味着市场预期现货下跌,现货收益会把展期收益抵消掉,期货超额收益的期望仍是零。只有当期货价格系统性低于未来现货期望(Keynes 的正常贴水理论,即存在风险溢价)时,carry 才是可交易的溢价。Koijen、Moskowitz、Pedersen 与 Vrugt(2018)在多资产类别中记录了 carry 溢价,但它是一个需要检验的经验问题,不是会计恒等式。

推导拆解:分解式的来历。对持有成本公式取对数:\(\ln F(t,T)=\ln S_t+(r+u-y_t)(T-t)\)。对 \(t\) 求变化(\(r\)、\(u\) 视为常数):\(\ln S_t\) 的变化照搬;\((r+u-y_t)(T-t)\) 是两个随时间变化的因子相乘,用乘积求导法则得到两项,\((T-t)\) 每天减少 \(dt\) 给出 \(-(r+u-y_t)dt\),\(y_t\) 的变化给出 \(-(T-t)dy_t\)。前者就是展期收益,后者是便利收益变化造成的曲线形状变化。 金融直觉:这和 CFA 里外汇远期的"利差交易"完全同构。远期汇率折价不代表未来即期汇率一定会贬值到那个水平,如果利率平价下远期是未来即期的无偏预测,高息货币的利差收益会被它的贬值抵消;只有存在风险溢价(远期偏离未来即期期望)时,carry 交易才赚钱。代码 9.5 的两个世界正是在模拟这两种情形:曲线形状完全一样,区别只在现货的真实漂移参数 \(\kappa\),这是无法从期货曲线本身读出来的。

9.6.3 代码:拼接、分解与两个世界里的 carry 策略

# 代码 9.5  期货连续合约、展期收益分解与 carry 策略
import numpy as np
import pandas as pd

def make_world(kappa, seed, n_com=8, years=25, r=0.03, u=0.02, sig=0.25):
    """kappa=0:期货价是未来现货的无偏预测(无展期溢价);kappa>0:贴水越深,期货越被低估"""
    rng = np.random.default_rng(seed)
    n = 252 * years; dt = 1 / 252
    ybar = np.linspace(-0.04, 0.12, n_com)                     # 各商品的长期便利收益率
    y = np.empty((n, n_com)); lnS = np.empty((n, n_com)); y[0] = ybar; lnS[0] = np.log(100)
    for t in range(1, n):
        y[t] = y[t-1] + 0.3 * (ybar - y[t-1]) * dt + 0.06 * np.sqrt(dt) * rng.standard_normal(n_com)
        mu = r + u - y[t-1] + kappa * (y[t-1] - ybar.mean())   # 现货的真实漂移
        lnS[t] = lnS[t-1] + (mu - 0.5 * sig**2) * dt + sig * np.sqrt(dt) * rng.standard_normal(n_com)
    expiries = np.arange(21, n + 63, 21)                         # 每 21 个交易日一个到期日
    lnF = lambda t, T: lnS[t] + (r + u - y[t]) * (T - t) * dt    # 持有成本公式(第 08 册第 05 章)
    return dict(n=n, y=y, lnS=lnS, lnF=lnF, expiries=expiries, r=r, u=u, n_com=n_com)

def rolled_series(W, roll_before=5):
    """持有近月,到期前 roll_before 天换到下一个合约;返回日度超额收益(对数)和"朴素拼接"价格"""
    n, lnF, ex = W["n"], W["lnF"], W["expiries"]
    held = np.empty(n, dtype=int)
    for t in range(n):
        held[t] = ex[np.searchsorted(ex, t + roll_before + 1)]   # 当日收盘后持有的合约到期日
    ret = np.zeros((n, W["n_com"])); naive = np.zeros((n, W["n_com"]))
    for t in range(n):
        naive[t] = lnF(t, held[t])
        if t > 0:
            ret[t] = lnF(t, held[t-1]) - lnF(t-1, held[t-1])     # 同一个合约的价格变化
    return ret, naive, held

# ---------- 1) 单个品种:朴素拼接 vs 正确展期,以及"展期收益分解" ----------
W = make_world(kappa=0.0, seed=5)
ret, naive, held = rolled_series(W)
i = 7                                                             # 便利收益最高(深度贴水)的品种
naive_ret = np.diff(naive[:, i])
roll_days = np.where(np.diff(held) != 0)[0] + 1
print(f"展期次数 {len(roll_days)};朴素拼接在展期日的平均'跳空' {1e4*naive_ret[roll_days-1].mean():.1f}bp")
spot_ret = np.diff(W["lnS"][:, i]).sum()
roll_yield = ((W["y"][:-1, i] - W["r"] - W["u"]) / 252).sum()
print(f"25 年累计(对数):展期期货 {ret[:, i].sum():+.3f} ≈ 现货 {spot_ret:+.3f} + 展期收益 {roll_yield:+.3f}"
      f" = {spot_ret + roll_yield:+.3f};朴素拼接 {naive_ret.sum():+.3f}")
# 后复权:把每个展期跳空从历史中扣掉,使价差序列的日变化等于真实持仓收益
adj = naive[:, i].copy()
for d in roll_days[::-1]:
    adj[:d] += naive[d, i] - W["lnF"](d, held[d-1])[i]
print("后复权序列的日变化与持仓收益的最大差:", np.abs(np.diff(adj) - ret[1:, i]).max())

# ---------- 2) 截面 carry 策略:两个世界的对比 ----------
for kappa in (0.0, 0.6):
    res = []
    for seed in range(20):
        W = make_world(kappa, seed)
        ret, _, held = rolled_series(W)
        rolls = np.where(np.diff(held) != 0)[0] + 1
        for a, b in zip(rolls[:-1], rolls[1:]):
            t0 = a - 1                                            # 用展期前一天收盘的曲线算 carry
            carry = W["y"][t0] - W["r"] - W["u"]                  # = (lnF1 - lnF2)/(T2-T1)
            order = np.argsort(carry)
            long_, short_ = order[-3:], order[:3]
            res.append(ret[a:b+1, long_].sum(0).mean() - ret[a:b+1, short_].sum(0).mean())
    res = np.array(res)
    print(f"kappa={kappa}: carry 多空月均 {100*res.mean():+.3f}%  年化 Sharpe {res.mean()/res.std()*np.sqrt(12):+.2f}"
          f"  t={res.mean()/res.std()*np.sqrt(len(res)):+.2f}  (20 个独立世界, {len(res)} 个月)")

输出:

展期次数 300;朴素拼接在展期日的平均'跳空' -68.3bp
25 年累计(对数):展期期货 -1.426 ≈ 现货 -3.588 + 展期收益 +2.167 = -1.421;朴素拼接 -3.592
后复权序列的日变化与持仓收益的最大差: 3.1086244689504383e-15
kappa=0.0: carry 多空月均 +0.074%  年化 Sharpe +0.04  t=+0.96  (20 个独立世界, 5980 个月)
kappa=0.6: carry 多空月均 +0.952%  年化 Sharpe +0.55  t=+12.28  (20 个独立世界, 5980 个月)

第一段:对深度贴水的品种,朴素拼接在每次换月日平均产生 −68bp 的跳空。25 年里朴素拼接的累计对数收益(−3.59)和现货几乎一样,而真实持仓收益是 −1.43,二者差了 2.17,正好是累计展期收益。也就是说,用朴素拼接序列回测,会系统性地漏掉(或者在升水品种中凭空加上)展期收益。后复权序列的日变化与真实持仓收益的差在 \(10^{-15}\) 量级。

第二段:两个世界中,按 carry 做多最高 3 个、做空最低 3 个品种。在期货价格无偏的世界(\(\kappa=0\)),carry 多空月均 0.07%,\(t=0.96\),统计上为零,尽管多头的展期收益每年比空头高出十几个百分点。在期货价格低估贴水品种的世界(\(\kappa=0.6\)),carry 策略的年化 Sharpe 为 0.55。两个世界的期货曲线形状完全一样,区别只在现货的真实漂移,这是从曲线本身无法看出来的。所以 carry 因子必须像其他因子一样,用本册第 03 章的方法检验其截面预测力,而不能因为"展期收益为正"就当成白给的收益。

与真实数据的差别:真实期货曲线的便利收益在不同期限上不同(季节性商品尤其明显),换月规则要按每个品种的流动性转移日设定;保证金占用和涨跌停也会改变可实现收益。

9.7 尾部风险对冲

9.7.1 几种工具

尾部对冲的目标是在市场崩盘时限制组合亏损。常用工具有:

  • 滚动买入虚值看跌期权:保护最直接,代价是持续的期权费。因为股指偏斜,虚值看跌的隐含波动率高于平值(第 08 册第 20.3 节),而且叠加了方差风险溢价,所以长期看是负期望的。
  • 看跌价差:买入较高执行价的看跌、卖出较低执行价的看跌,用放弃极端尾部的保护来降低成本。
  • 动态策略:波动率目标(波动上升时降仓)、趋势跟随、组合保险。它们不付期权费,但保护依赖于下跌是否给出调整的时间。1987 年股灾中组合保险的失败(第 08 册第 19a 章 19.14 节、第 07 册第 28 章)说明,动态复制在跳空和流动性枯竭时会失效。

评价尾部对冲不能只看"崩盘那个月赚了多少",而要同时看长期成本、对最大回撤的影响和在什么样的下跌中有效。

9.7.2 代码:看跌、价差与波动率目标的比较

沿用 9.4 节的价格机制,模拟 40 年。看跌期权按"平值隐含波动率 × (1 + 0.9 × ln(S/K))"定价,平值隐含波动率含 1.35 倍的方差溢价;每月滚动买入 1 个月期限、名义金额等于组合价值的看跌。波动率目标策略用前一天的 EWMA 波动率(第 06 册第 07a 章 RiskMetrics,λ=0.94)把仓位调整到年化 12%,最高 1.5 倍杠杆,计入单边 2bp 调仓成本。

# 代码 9.6  尾部风险对冲:虚值看跌、看跌价差与波动率目标
import numpy as np
from scipy.stats import norm

def bs_put(S, K, T, r, sig):
    s = sig * np.sqrt(T); d1 = (np.log(S / K) + (r + 0.5 * sig**2) * T) / s
    return K * np.exp(-r * T) * norm.cdf(-d1 + s) - S * norm.cdf(-d1)

# ---------- 合成指数:GJR-GARCH + t5 + 负跳跃(与 9.4 节同一机制,换一个种子、拉长到 40 年) ----------
rng = np.random.default_rng(99)
n, H, r = 252 * 40, 21, 0.03
omega, a, g, b, nu = 1.5e-6, 0.02, 0.12, 0.90, 5
lam, mu_j, sd_j = 1 / 500, -0.06, 0.03
z = rng.standard_t(nu, n) / np.sqrt(nu / (nu - 2))
J = (rng.random(n) < lam) * rng.normal(mu_j, sd_j, n)
h = np.empty(n); ret = np.empty(n); h[0] = omega / (1 - a - g / 2 - b)
for t in range(n):
    if t:
        e = ret[t-1] - J[t-1]; h[t] = omega + (a + g * (e < 0)) * e**2 + b * h[t-1]
    ret[t] = 0.0004 + np.sqrt(h[t]) * z[t] + J[t]
pers = a + g / 2 + b; hbar = omega / (1 - pers)
def atm_iv(hn):        # 未来 21 天条件期望方差 → 年化,再乘 1.35 的方差溢价
    ev = np.sum(hbar + pers**np.arange(H) * (hn - hbar)) + H * lam * (mu_j**2 + sd_j**2)
    return np.sqrt(1.35 * ev * 252 / H)
skew = 0.9             # IV(K) = ATM_IV × (1 + skew × ln(S/K)),10% 虚值看跌约贵 9.5%

starts = np.arange(0, n - H, H)
R_idx = np.array([np.expm1(ret[s:s+H].sum()) for s in starts])
def put_overlay(Kpct, Kpct2=None, notional=1.0):
    out = []
    for s, R in zip(starts, R_idx):
        iv0 = atm_iv(h[s]); T = H / 252
        p1 = bs_put(1.0, Kpct, T, r, iv0 * (1 + skew * np.log(1 / Kpct)))
        pay = max(Kpct - (1 + R), 0)
        if Kpct2 is not None:             # 卖出更低执行价的看跌,组成看跌价差
            p1 -= bs_put(1.0, Kpct2, T, r, iv0 * (1 + skew * np.log(1 / Kpct2)))
            pay -= max(Kpct2 - (1 + R), 0)
        out.append(notional * (pay - p1 * np.exp(r * T)))   # 期权费按月末价值计
    return np.array(out)

# 波动率目标:用前一日 EWMA 波动率把仓位调到年化 12%(与指数长期波动相当),上限 1.5 倍杠杆,日度再平衡
lam_ew = 0.94; v = np.empty(n); v[0] = ret[:60].var()
for t in range(1, n): v[t] = lam_ew * v[t-1] + (1 - lam_ew) * ret[t-1]**2
w = np.clip(0.12 / np.sqrt(252 * v), 0, 1.5)
daily_vt = w * np.expm1(ret) + (1 - w) * r / 252 - 0.0002 * np.abs(np.diff(w, prepend=w[0]))
R_vt = np.array([np.prod(1 + daily_vt[s:s+H]) - 1 for s in starts])

def report(name, Rm):
    eq = np.cumprod(1 + Rm); dd = (eq / np.maximum.accumulate(eq) - 1).min()
    cagr = eq[-1]**(12 / len(Rm)) - 1; vol = Rm.std() * np.sqrt(12)
    es = -np.sort(Rm)[:int(0.05 * len(Rm))].mean()
    print(f"{name:16s} 年化 {100*cagr:5.2f}%  波动 {100*vol:5.2f}%  最大回撤 {100*dd:6.1f}%  "
          f"最差月 {100*Rm.min():6.1f}%  月度 ES95 {100*es:5.2f}%  Calmar {cagr/-dd:4.2f}")

ov90 = put_overlay(0.90); ov95 = put_overlay(0.95); ovps = put_overlay(0.95, 0.85)
report("指数", R_idx)
report("指数+90%看跌", R_idx + ov90)
report("指数+95%看跌", R_idx + ov95)
report("指数+95/85价差", R_idx + ovps)
report("波动率目标12%", R_vt)
for nm, ov in (("90%看跌", ov90), ("95%看跌", ov95), ("95/85价差", ovps)):
    print(f"{nm}: 年均净成本 {-12*100*ov.mean():.2f}%,有正收益的月份占比 {100*np.mean(ov>0):.1f}%")
crash = np.argsort(R_idx)[:5]
print("指数最差 5 个月:指数 / +95%看跌 / 波动率目标")
for i in sorted(crash):
    print(f"  第{i:3d}月  {100*R_idx[i]:6.1f}%  {100*(R_idx[i]+ov95[i]):6.1f}%  {100*R_vt[i]:6.1f}%")

输出:

指数               年化  9.97%  波动 12.43%  最大回撤  -30.2%  最差月  -18.1%  月度 ES95  6.76%  Calmar 0.33
指数+90%看跌         年化  9.82%  波动 12.08%  最大回撤  -31.1%  最差月  -10.8%  月度 ES95  6.50%  Calmar 0.32
指数+95%看跌         年化  8.14%  波动 11.50%  最大回撤  -32.8%  最差月   -6.8%  月度 ES95  5.42%  Calmar 0.25
指数+95/85价差       年化  8.14%  波动 11.62%  最大回撤  -32.0%  最差月   -9.6%  月度 ES95  5.51%  Calmar 0.25
波动率目标12%         年化 11.26%  波动 12.21%  最大回撤  -33.2%  最差月  -11.5%  月度 ES95  7.18%  Calmar 0.34
90%看跌: 年均净成本 0.18%,有正收益的月份占比 0.2%
95%看跌: 年均净成本 1.80%,有正收益的月份占比 4.4%
95/85价差: 年均净成本 1.78%,有正收益的月份占比 4.4%
指数最差 5 个月:指数 / +95%看跌 / 波动率目标
  第  9月    -8.4%    -5.1%    -9.8%
  第147月    -8.5%    -5.2%    -9.4%
  第165月    -7.5%    -5.4%    -4.7%
  第284月    -7.3%    -5.1%   -11.5%
  第448月   -18.1%    -6.8%    -7.6%

这组结果值得逐条读。

10% 虚值看跌每年只花 0.18%,但 40 年中只有 0.2% 的月份有正收益,几乎只在那次 −18% 的崩盘月起作用,把最差月从 −18.1% 降到 −10.8%,对最大回撤没有帮助。5% 虚值看跌更有效,最差月降到 −6.8%,月度 ES 从 6.8% 降到 5.4%,但每年成本 1.8%,年化收益从 10.0% 降到 8.1%,而最大回撤反而从 −30.2% 变成 −32.8%。原因是这个模拟中的最大回撤由连续多个月的中等幅度下跌累积而成,每个月都没有跌破执行价,看跌期权一分钱都没赔,期权费却每月照付。看跌价差的成本与 5% 看跌接近,保护更弱,在这组参数下没有优势。

波动率目标没有期权费,年化收益反而更高(11.3%),最差月也降到 −11.5%,但最大回撤(−33.2%)和月度 ES 并不比指数好。收益更高是这个模型的特性:模拟中预期收益不随波动率变化,平静期加杠杆提高了风险调整收益;真实市场中这一点有证据支持,但幅度存在争议。它对跳空型崩盘的保护有限,因为降仓要等波动率估计上升之后。

结论是:期权对冲买的是"单月极端损失"的保护,费用确定;动态策略的保护依赖下跌路径,费用不确定。要控制回撤,单靠滚动短期看跌是昂贵且不完整的,需要和仓位管理、回撤规则(本册第 11 章 11.5 节)一起设计。选择执行价和期限时,应当先明确要保护的损失类型(单日跳空、单月暴跌、慢熊),再用情景分析比较成本。

金融直觉:用保险条款来读这组结果最清楚。每月滚动买 1 个月期的看跌,相当于买一份"每月独立结算、免赔额为 5%(或 10%)的单月保险"。95% 看跌的免赔额是单月跌幅 5%,90% 看跌是 10%。一段由多个月、每月跌 4% 累积起来的慢熊,每个月都在免赔额以内,保险一分不赔,保费照付,所以最大回撤反而更深。看跌价差则是在此基础上再加一个"赔付上限"(跌超 15% 的部分不赔),用放弃巨灾赔付换取更低的保费。 代码里的定价方式也值得注意:看跌的隐含波动率 \(=\) 平值 IV \(\times(1+0.9\ln(S/K))\),行权价越低越贵,这模拟了股指的偏斜;平值 IV 本身含 1.35 倍的方差溢价。也就是说,买保护的人同时在付两份溢价(方差溢价和偏斜溢价),这正是 9.4 节卖方赚的钱。同一份溢价,在做市台的账本上是收入,在资产配置委员会的账本上是成本。

与真实数据的差别:真实崩盘中隐含波动率会跳升,按市值计价的看跌期权在到期前就能大幅升值,可以提前兑现(本例只计到期收益,低估了看跌的保护);危机时偏斜变陡,滚动买入的成本也会上升;动态策略在流动性枯竭时的执行成本远高于 2bp。

9.8 常见陷阱与检查清单

定价与 IV

  • 用 \(\phi\) 统一看涨看跌;用 BSM PDE 残差和平价检查希腊字母实现。
  • 求 IV 前先检查无套利边界;对 vega 过小的期权返回缺失值而不是一个数字。
  • 单位要统一:vega 是"每单位 σ"还是"每个波动率点",theta 是"每年"还是"每日",在接口里写清楚。
  • 美式期权和离散股息不能直接套 BSM。

曲面

  • 远期用平价回归,不用"现货 × 利率";只用虚值一侧;按价差或 vega 加权。
  • 在总方差上插值,检查日历和蝶式套利;外推时检查两翼斜率不超过 Lee 矩公式的上限(总方差斜率不超过 2)。
  • 信号要在常数期限上比较。

波动率策略

  • 卖方差、卖期权一类策略,必须报告偏度、最差月、ES 和压力情景;用最坏情景而不是历史波动率决定规模。
  • 盈亏分解的残差要监控,残差突然变大往往意味着对冲引擎、希腊字母或数据出错。
  • 对冲频率要和交易成本一起优化,考虑带宽规则。

期货

  • 回测只用同一合约的价格变化计算收益;指标用后复权序列;换月规则按流动性设定。
  • 展期收益不是预期收益,carry 要做预测力检验。

尾部对冲

  • 明确要保护的损失类型;同时比较长期成本、最差月、最大回撤和 ES。
  • 动态策略在跳空中失效,期权在慢熊中失效。

本章小结

本章把第 08 册的衍生品理论落到了研究和交易流程中。定价引擎的关键是统一写法、PDE 残差检查和带括号保护的向量化牛顿法;IV 的可信度由 vega 决定。构建曲面时,用平价回归估计远期,用虚值报价拟合每个期限的 SVI,并在总方差上插值、检查无套利。方差风险溢价让卖方差在大多数时间赚钱,但收益高度负偏,最差几个月决定了策略的命运。Delta 对冲期权的盈亏可以分解为 dollar gamma 加权的"实现方差减隐含方差",对冲频率只能降低离散化误差,不能消除波动率本身的不确定性。期货回测要用同一合约的收益,展期收益只有在期货价格有偏时才是溢价。尾部对冲买的是单月极端损失的保护,控制最大回撤需要仓位管理配合。

练习

  1. IV 的有效位数。 在 9.2.3 的代码中,把报价精度从 0.01 改为 0.05,并对每个期权计算"一个 tick 对应的 IV 变化"\(=0.05/\text{vega}\)。画出它与 \(|k|/\sqrt T\) 的关系,并据此给出剔除规则。提示:标准化的虚值程度 \(k/(\sigma\sqrt T)\) 比绝对的 \(k\) 更能说明问题。

  2. 制造并修复蝶式套利。 在 9.3.2 的代码中,只保留 21 天期限 \(|k|<0.05\) 以内的 8 个报价,再拟合 SVI,检查 \(g(k)\) 在 \(k\in[-0.6,0.4]\) 上的最小值。然后在 least_squares 的残差中加入罚项 \(\lambda\sum\max(-g(k_j),0)\),比较罚项前后的拟合误差和外推形状。提示:两翼没有数据时,\(b\) 往往被推到很大。

  3. 由曲面计算方差互换执行价。 用 9.3.2 拟合的 63 天 SVI,在密集的行权价网格上生成虚值期权价格,按第 08 册第 26b 章式 (26.6) 计算公平方差,与平值隐含方差比较。提示:偏斜越陡,公平方差比平值方差高得越多;行权价网格截断会低估公平方差。

  4. VRP 择时。 在 9.4.2 的模拟中,把方差溢价改为随状态变化,例如 \(1+0.5\cdot\mathbb 1(\text{上月跌幅}>5\%)\)。比较"始终卖方差"和"只在 IV 与 GARCH 预测之差高于中位数时卖方差"的 Sharpe、偏度和最差月。提示:择时信号所需的 GARCH 预测必须只用当时已知的数据估计(本册第 06 章,前视偏差)。

  5. 带宽对冲。 修改 9.5.2 的 hedge 函数,只在 \(|\Delta_{new}-\Delta_{held}|>b\) 时调仓,在每日 16 次检查的频率下比较 \(b=0.01,0.03,0.05,0.1\) 的含成本均值和标准差。提示:把结果画在"成本–标准差"平面上,找有效前沿。

  6. 展期规则的影响。 在 9.6.3 的代码中,把 roll_before 从 5 改为 0、10、15,比较深度贴水品种的累计收益。在本模拟中差别从何而来?在真实市场中还会有什么额外差别?提示:本模拟中曲线是平坦的 \(y\),真实曲线近端往往更陡。

  7. 提前兑现的看跌。 修改 9.7.2 的代码,每天用当时的平值 IV 和偏斜给持有的看跌按市值计价;当看跌价值超过买入价的 5 倍时卖出并买入新的看跌。比较最大回撤和年化成本的变化。

延伸阅读

  • 第 08 册第 15a、15b 章(BSM 推导与隐含波动率)、第 19a、19b 章(希腊字母与 delta 对冲盈亏公式)、第 20 章(波动率微笑与 Breeden–Litzenberger)、第 26b 章(方差互换与 VIX)、第 27a 章(随机波动率与局部波动率)、第 05 章(持有成本)、第 03 章(套期保值与展期)。
  • 第 06 册第 03a、03b 章(GARCH 与已实现波动率)、第 07a 章(RiskMetrics)。
  • 第 07 册第 22b 章(比索问题与偏度)、第 28 章(1987 年崩盘与组合保险)。
  • Gatheral, J. (2006). The Volatility Surface: A Practitioner's Guide. Wiley.
  • Gatheral, J., & Jacquier, A. (2014). Arbitrage-free SVI volatility surfaces. Quantitative Finance, 14(1).
  • Demeterfi, K., Derman, E., Kamal, M., & Zou, J. (1999). More than you ever wanted to know about volatility swaps. Goldman Sachs Quantitative Strategies Research Notes.
  • Carr, P., & Wu, L. (2009). Variance risk premiums. Review of Financial Studies, 22(3).
  • Bakshi, G., & Kapadia, N. (2003). Delta-hedged gains and the negative market volatility risk premium. Review of Financial Studies, 16(2).
  • Leland, H. (1985). Option pricing and replication with transactions costs. Journal of Finance, 40(5).
  • Koijen, R., Moskowitz, T., Pedersen, L., & Vrugt, E. (2018). Carry. Journal of Financial Economics, 127(2).