1993・Heston 隨機波動:微笑曲線的隨機解#
「Black–Scholes 以為波動是常數,市場卻笑出了一條曲線。」 本案時間:1993 年,地點:美國耶魯,報案人:外匯與股票選擇權交易員。
| 案件檔案 | 內容 |
|---|---|
| 案號 | SV-1993-Heston |
| 年份 | 1993 年 |
| 主角 | Steven Heston |
| 關鍵文獻 | A Closed-Form Solution for Options with Stochastic Volatility(1993) |
| 涉案對象 | 價格 \(S\) 、變異數 \(v\) 、相關係數 \(\rho\) 、波速 \(\xi\) |
| 核心證物 | CIR 型變異數過程、半封閉特徵函數、Fourier 反演定價 |
| 關聯舊案 | 1973 年 Black–Scholes、1979 年鞅定價、1992 年數值聖經 |
| 遺產 | 外匯微笑標準模型、粗糙波動的前輩、校準工業的起點 |
案發現場#
1987 年股災之後,選擇權市場留下了一道洗不掉的微笑。
Black–Scholes 公式假設波動率 \(\sigma\) 是常數,推出的隱含波動應該是一條水平線。但交易螢幕上,深價外賣權的隱含波動明顯上翹,偏度與峰度公然嘲笑常態假設。交易員被迫用一張波動曲面打補丁:每個履約價配一個波動,模型淪為插值器。
此前的嫌疑犯各有缺陷。Merton 的跳躍模型能造出厚尾,但跳躍難對沖;Hull–White 的隨機波動直覺正確,卻只能依賴蒙地卡羅或近似;Stein–Stein 模型把波動本身寫成 OU 過程,波動卻可能變負,物理上說不通。
Heston 接案時開出的條件很硬:波動必須恆正,模型必須能算,最好有接近封閉的定價公式,能直接校準到市場微笑。少一條,就不算破案。
偵查過程#
偵探把價格與變異數寫成一對連動的 SDE。設價格為 \(S\) ,瞬時變異數為 \(v\) ,兩組布朗運動相關係數為 \(\rho\) :
第二行是 CIR 型(平方根)過程: \(\kappa\) 是均值回歸速度, \(\bar v\) 是長期均值, \(\xi\) 是波動的波動。平方根 \(\sqrt{v}\) 是神來之筆——變異數貼近零時噪聲自動熄火,配合 Feller 條件就不會穿越零點。
Feller 條件是本案的第一道封條:
成立時,變異數過程恆正;違反時,原點可達,數值模擬會頻繁撞零。實務上市場參數常踩線,於是有了全套截斷與反射修補,那是 1992 年數值聖經也頭痛的硬骨頭。
第二步是定價。風險中性下寫出二維 PDE,傳統有限差分直接硬解,維度一高就喘。Heston 改走特徵函數路線:猜對數價格的條件特徵函數有指數仿射形式,偏微分方程坍縮成一組 Riccati 常微分方程,可半封閉求解。再經 Fourier 反演,選擇權價格化為兩個「尾機率」之差:
其中 \(P_1\) 與 \(P_2\) 由特徵函數 \(\phi\) 的積分給出。這就是所謂半封閉解:Riccati 部分解析,反演積分留給數值求積。
下表是參數的側寫檔案,每個參數都是微笑曲線的一名整形醫生:
| 參數 | 符號 | 整形效果 |
|---|---|---|
| 相關 | \(\rho\) | 控制偏度,負值造出權益市場的左偏微笑 |
| 波速 | \(\xi\) | 控制峰度,越大微笑兩端翹得越高 |
| 回歸速 | \(\kappa\) | 控制期限結構,越大短期微笑衰減越快 |
| 長期均值 | \(\bar v\) | 控制曲面高度,整體上下平移 |
| 初值 | \(v_0\) | 控制短期 at-the-money 水位 |
相關係數 \(\rho\) 的符號尤其關鍵。股票市場多為負相關:跌時波動飆升,左尾肥大,微笑左高右低。外匯市場 \(\rho\) 近零,微笑近乎對稱。一組參數同時解釋偏度、峰度與期限結構,這是 Heston 勝過局部波動模型的探案亮點。
結案報告#
1993 年一案,用五個參數馴服了微笑。
Heston 模型成為外匯與權益衍生品的工業標準:校準快、能對沖、能報價。Duffie–Pan–Singleton 後來把它收編進仿射框架,Bates 再把跳躍接回來,家族開枝散葉。即使 2014 年粗糙波動指控「波動比布朗更粗糙」,Heston 仍是每個新模型的必考對手。
在歷史年表裡,本案是 1973 年 Black–Scholes 的嫡傳續集:從常數波動到隨機波動,從封閉公式到半封閉特徵函數。它也反哺了數值學:平方根擴散的離散格式,至今仍是 Kloeden–Platen 法典裡的經典難題。
未竟之業是校準的穩定性。五個參數辨識不易,Riccati 的複對數分支切割曾引發著名的「Heston 陷阱」,後由 Gatheral 與 Albrecher 等人修補。微笑被解釋了,但沒有被消滅——它只是換了一組參數繼續微笑。
案件狀態:主犯落網,微笑收押,餘黨(跳躍與粗糙)另案偵辦。
證據與工具#
證物一,風險中性 Heston 的標準寫法( \(r\) 為利率):
星號標記風險中性測度。注意漂移可變,擴散結構不變,這正是 Girsanov 定理的承諾。
證物二,特徵函數結構速記。設 \(x = \ln S_T\) ,則:
函數 \(C\) 與 \(D\) 滿足 Riccati 系統,初值 \(C = D = 0\) 。記住「仿射進、仿射出」,就不會在推導中迷路。
證物三,實務工具箱:
| 工具 | 用途 | 備註 |
|---|---|---|
| Fourier 反演求積 | 由 \(\phi\) 算 \(P_1, P_2\) | 注意阻尼因子與分支切割 |
| Feller 檢查 | \(2\kappa\bar v \ge \xi^2\) | 違反時改用截斷 Euler 或 QE 格式 |
| 賣買權平價 | 模型無關驗算 | 對應程式 _code/1993-heston_mc.py |
探員建議:先用平價驗程式,再校準微笑,最後才信任對沖比。順序錯了,微笑會反咬你一口。
補充:程式實作#
對應程式#
- 1993-heston_mc.py:以全截斷 Euler 加 log-Euler 蒙地卡羅模擬 Heston 模型,並用賣買權平價驗收。
理論呼應#
本文模型為 \(dS = \mu S\,dt + \sqrt{v}S\,dW^S\) 配 CIR 型變異數 \(dv = \kappa(\bar v - v)\,dt + \xi\sqrt{v}\,dW^v\) ,相關結構為 \(dW^S\,dW^v = \rho\,dt\) 。 平方根擴散使變異數貼近零時噪聲自動熄火,Feller 條件 \(2\kappa\bar v \ge \xi^2\) 則是恆正的封條,程式以 \(v^+ = \max(v, 0)\) 做全截斷正是對應的數值修補。 定價半封閉解形如 \(C = S_0P_1 - Ke^{-rT}P_2\) ,而任何相容定價都須先過模型無關的賣買權平價 \(C - P = S_0 - Ke^{-rT}\) ,程式即以此式驗收。
執行方式#
在 隨機微積分 目錄下執行 python3 _code/1993-heston_mc.py 。
實測輸出#
Call = 9.226585, Put = 6.304900
parity LHS(C-P) = 2.921685, RHS = 2.955447
abs_err = 0.033761, rel_err = 0.000338 (tol 0.01)
VERIFY rel_parity_err=0.000338 C=9.2266 P=6.3049
讀者實驗#
將相關係數 rho 由 -0.7 改為 0.0 再重跑,觀察 Call 與 Put 價格及平價誤差是否仍過關並體會偏度消失的對稱微笑。
完整程式如下:
"""1993 Heston 隨機波動率:全截斷 Euler 蒙地卡羅。
對應 wiki:Heston (1993) 閉式解 / 特徵函數定價;此處以蒙地卡羅驗證。
模型:dv = κ(θ-v)dt + ξ√v dWv, dS/S = r dt + √v dWs, Corr = ρ。
參數:S0=100, v0=0.04, κ=2, θ=0.04, ξ=0.3, ρ=-0.7;
定價參數:r=0.03, K=100, T=1.0(wiki 常見 ATM 基準)。
格式:變異數全截斷 v+ = max(v,0);資產採 log-Euler
S *= exp((r-0.5 v+)dt + √(v+ dt) Zs),保證 S > 0。
驗證:同路徑 Call/Put 滿足 put-call parity
C - P = S0 - K exp(-rT),相對誤差(除以 S0) < 1%;
且價格為正有限。
只用 numpy,固定種子,不畫圖。
"""
import numpy as np
rng = np.random.default_rng(1)
S0 = 100.0
v0 = 0.04
kappa = 2.0
theta = 0.04
xi = 0.3
rho = -0.7
r = 0.03
K = 100.0
T = 1.0
N = 100
dt = T / N
M = 100000
sqdt = np.sqrt(dt)
v = np.full(M, v0)
S = np.full(M, S0)
corr = np.sqrt(1.0 - rho ** 2)
for _ in range(N):
z1 = rng.standard_normal(M)
z2 = rng.standard_normal(M)
zv = z1
zs = rho * z1 + corr * z2
vpos = np.maximum(v, 0.0)
sqv = np.sqrt(vpos)
v = v + kappa * (theta - vpos) * dt + xi * sqv * sqdt * zv
S = S * np.exp((r - 0.5 * vpos) * dt + sqv * sqdt * zs)
disc = np.exp(-r * T)
C = float(np.mean(np.maximum(S - K, 0.0)) * disc)
P = float(np.mean(np.maximum(K - S, 0.0)) * disc)
lhs = C - P
rhs = S0 - K * np.exp(-r * T)
err = abs(lhs - rhs)
rel = err / S0
print(f"Call = {C:.6f}, Put = {P:.6f}")
print(f"parity LHS(C-P) = {lhs:.6f}, RHS = {rhs:.6f}")
print(f"abs_err = {err:.6f}, rel_err = {rel:.6f} (tol 0.01)")
print(f"VERIFY rel_parity_err={rel:.6f} C={C:.4f} P={P:.4f}")
assert np.isfinite(C) and np.isfinite(P), "價格非有限"
assert C > 0 and P > 0, "價格須為正"
assert rel < 0.01, f"put-call parity 誤差過大: {rel}"