Heston COS Method¶
Background¶
Heston Cos Method
Educational script demonstrating heston cos method concepts.
Code¶
```python """ Heston Cos Method
Educational script demonstrating heston cos method concepts. """
@title Heston Call and Put COS Method¶
Source Code¶
import matplotlib.pyplot as plt import numpy as np import scipy.stats as st
======================================================================¶
def BS_Call_Option_Price(CP,S_0,K,sigma,tau,r): d1 = ( np.log(S_0/K) + (r+0.5sigma2tau) ) / (sigmanp.sqrt(tau)) d2 = d1 - sigma * np.sqrt(tau) if str(CP).lower()=="c" or str(CP).lower()=="1": value = S_0 * st.norm.cdf(d1) - Knp.exp(-rtau) * st.norm.cdf(d2) elif str(CP).lower()=="p" or str(CP).lower()=="-1": value = Knp.exp(-r*tau) * st.norm.cdf(-d2) - S_0 * st.norm.cdf(-d1) return value
def dV_dsigma(S_0,K,sigma,tau,r): """ Vega = \frac{\partial V}{\partial \sigma} """ d2 = ( np.log(S_0/K) + (r-0.5sigma2tau) ) / (sigmanp.sqrt(tau)) return Knp.exp(-r*tau) * st.norm.pdf(d2) * np.sqrt(tau)
def ImpliedVolatility(CP,S_0,K,sigma,tau,r,V_market): # Handy lambda expressions optPrice = lambda sigma: BS_Call_Option_Price(CP,S_0,K,sigma,tau,r) vega = lambda sigma: dV_dsigma(S_0,K,sigma,tau,r)
# Newton-Raphson method
n = 1
error = 1e10 # initial error
while error>10e-9:
g = optPrice(sigma) - V_market
g_prim = vega(sigma)
sigma_new = sigma - g / g_prim
#error = abs(sigma_new-sigma) # alternative error that can be used
error = abs(g)
sigma = sigma_new;
print(f'Iteration {n:05} Error {error:10.8f}')
n = n+1
return sigma
def compute_ABC(r,u,tau,kappa,v_bar,gamma,rho): i = complex(0., 1.)
kappa_ = kappa - gamma * rho * i * u
D = np.sqrt( kappa_**2 + (u**2+i*u)*gamma**2 )
kappa_p = kappa_ + D
kappa_n = kappa_ - D
kappa_bar = kappa * v_bar / gamma**2
e = np.exp( - D * tau )
g = kappa_n / kappa_p
A = r*(i*u-1)*tau + kappa_bar*tau*kappa_n - 2*kappa_bar*np.log( (1-g*e) / (1-g) )
B = i*u
C = (1-e) * kappa_n / ( gamma**2*(1-g*e) )
return A, B, C
def compute_ChF(X,v,r,u,tau,kappa,v_bar,gamma,rho): A, B, C = compute_ABC(r,u,tau,kappa,v_bar,gamma,rho) return np.exp(rtau + A + BX + C*v)
def compute_A_k(chf,a,b,n): """ chf : characteristic function a : lower limit of the pdf support b : upper limit of the pdf support n : number of terms in Fourier cosine expansion """ k = np.arange(n) k_ = k * np.pi / (b-a)
i = complex(0.0,1.0)
A_k = 2.0 / (b - a) * np.real( chf(k_) * np.exp(-i*k_*a) );
A_k[0] = A_k[0] * 0.5; # adjustment for the first term
return A_k
def compute_chi_k_and_psi_k(a,b,c,d,n): """ a : lower limit of the pdf support b : upper limit of the pdf support c : lower limit of the option exercise support d : upper limit of the option exercise support n : number of terms in Fourier cosine expansion """ k = np.arange(n) k_pi = k * np.pi / ( b - a )
theta_c = ( c - a ) * k_pi
theta_d = ( d - a ) * k_pi
cos_ = np.cos(theta_d)*np.exp(d) - np.cos(theta_c)*np.exp(c)
sin_ = k_pi*np.sin(theta_d)*np.exp(d) - k_pi*np.sin(theta_c)*np.exp(c)
chi_k = ( cos_ + sin_ ) / ( 1 + k_pi**2 )
psi_k = chi_k.copy()
psi_k[1:] = ( np.sin(theta_d[1:]) - np.sin(theta_c[1:]) ) / k_pi[1:]
psi_k[0] = d - c
return chi_k, psi_k
def compute_H_k(a,b,n,K,cp): """ a : lower limit of the pdf support b : upper limit of the pdf support n : number of terms in Fourier cosine expansion K : strike cp : 'c' or 'p' representing call or put """ if cp == "c": chi_k, psi_k = compute_chi_k_and_psi_k(a,b,c=np.log(K),d=b,n=n) H_k = ( chi_k - K * psi_k ) * 2 / (b-a) if cp == "p": chi_k, psi_k = compute_chi_k_and_psi_k(a,b,c=a,d=np.log(K),n=n) H_k = ( K * psi_k - chi_k ) * 2 / (b-a) return H_k
def compute_OptionPriceCOSMethod(chf,x0,r,tau,K,a,b,n,cp): """ chf : characteristic function x0 : log S0 r : interest rate tau : time to maturity K : strikes as a list a : lower limit of the pdf support b : upper limit of the pdf support n : number of terms in Fourier cosine expansion cp : 'c' or 'p' representing call or put """ value = [] for K_temp in K: A_k = compute_A_k(chf,a,b,n) H_k = compute_H_k(a,b,n,K_temp,cp) value.append( (A_k * H_k).sum() * np.exp(-r*tau) * (b - a ) / 2 ) return value
def BS_Call_Option_Price(CP,S_0,K,sigma,tau,r):¶
#Black-Scholes Call option price¶
cp = str(CP).lower()¶
K = np.array(K).reshape([len(K),1])¶
d1 = (np.log(S_0 / K) + (r + 0.5 * np.power(sigma,2.0)) * tau) / float(sigma * np.sqrt(tau))¶
d2 = d1 - sigma * np.sqrt(tau)¶
if cp == "c" or cp == "1":¶
value = st.norm.cdf(d1) * S_0 - st.norm.cdf(d2) * K * np.exp(-r * tau)¶
elif cp == "p" or cp =="-1":¶
value = st.norm.cdf(-d2) * K * np.exp(-r * tau) - st.norm.cdf(-d1)*S_0¶
return value¶
def main(): S = 100 X = np.log(S) v = 0.32 r = 0.03 u = np.linspace(0,60,2500) tau = 1 / 12 kappa = 2 v_bar = 0.22 gamma = 0.4 rho = - 0.9 K = np.arange(100, 150, 10) #np.arange(90, 120, 10) #np.arange(50, 210, 10) # np.arange(90, 120, 10) #
u_max = 60.0
N = 2**10
x_min = 4.1
x_max = 5.1
# characteristic function
chf = lambda u : compute_ChF(X,v,r,u,tau,kappa,v_bar,gamma,rho)
c_1 = compute_OptionPriceCOSMethod(chf,x0=X,r=r,tau=tau,K=K,a=x_min,b=x_max,n=N,cp='c')
c_2 = BS_Call_Option_Price(CP='c',S_0=S,K=K,sigma=np.sqrt(v),tau=tau,r=r)
vol_c_1 = []
for c, K_tmp in zip(c_1,K):
vol_c_1.append(ImpliedVolatility('c',S,K_tmp,0.3,tau,r,V_market=c))
p_1 = compute_OptionPriceCOSMethod(chf,x0=X,r=r,tau=tau,K=K,a=x_min,b=x_max,n=N,cp='p')
p_2 = BS_Call_Option_Price(CP='p',S_0=S,K=K,sigma=np.sqrt(v),tau=tau,r=r)
vol_p_1 = []
for p, K_tmp in zip(p_1,K):
vol_p_1.append(ImpliedVolatility('p',S,K_tmp,0.3,tau,r,V_market=p))
fig, ((ax0, ax1), (ax2, ax3)) = plt.subplots(2,2,figsize=(12,8))
ax0.plot(K,c_1,'-*r',label='Call Option Price by COS Method')
ax0.plot(K,c_2,'-b',label='Call Option Price by BS Model')
ax1.plot(K,p_1,'-*r',label='Put Option Price by COS Method')
ax1.plot(K,p_2,'-b',label='Put Option Price by BS Model')
ax2.plot(K,vol_c_1,"-*r", label='Implied Volatility of Call')
ax3.plot(K,vol_p_1,"-*r", label='Implied Volatility of Put')
for ax in (ax0, ax1, ax2, ax3):
ax.set_xlabel('K')
ax.legend()
plt.tight_layout()
plt.show()
if name == "main": main() ```
Exercises¶
Exercise 1. The Heston characteristic function involves complex exponentials of Riccati solutions. Explain why the Schoutens (2004) formulation is numerically preferred over the original Heston (1993) form.
Solution to Exercise 1
The original Heston CF contains \(\exp(d \cdot t)\) terms where \(d\) is complex with potentially large positive real part for large \(u\) or \(t\). This causes numerical overflow. The Schoutens formulation rearranges to use \(\exp(-d \cdot t)\), which decays rather than grows. Both are algebraically equivalent but the Schoutens form avoids branch-cut discontinuities in the complex logarithm.
Exercise 2. For the COS method applied to Heston, the truncation interval \([a, b]\) must account for the fat-tailed Heston distribution. How should \(L\) be adjusted compared to the GBM case?
Solution to Exercise 2
Heston log-returns have excess kurtosis from stochastic volatility, requiring wider truncation: \(a = c_1 - L\sqrt{c_2 + \sqrt{c_4}}\), \(b = c_1 + L\sqrt{c_2 + \sqrt{c_4}}\) where \(c_1, c_2, c_4\) are the first, second, and fourth cumulants of the Heston distribution. Using \(L = 12\) (vs \(L = 10\) for GBM) accounts for the heavier tails.
Exercise 3. The COS method requires \(N\) evaluations of the Heston CF. If each evaluation takes \(O(1)\) time and \(N = 128\), estimate the speedup over Monte Carlo with \(10^5\) paths.
Solution to Exercise 3
COS: \(128\) CF evaluations plus matrix operations \(\approx 10^3\) floating-point operations. MC: \(10^5\) paths \(\times\) 252 time steps \(\times\) 10 operations/step \(\approx 2.5 \times 10^8\) operations. Speedup: \(\approx 2.5 \times 10^5\), or about 5 orders of magnitude. COS takes microseconds; MC takes seconds.
Exercise 4. The COS method prices options for multiple strikes simultaneously. If you need prices at 100 strikes, compare the cost of COS versus 100 separate BS formula evaluations.
Solution to Exercise 4
COS: \(N\) CF evaluations (\(\sim 128\)) plus one matrix multiply \(N \times K\) where \(K = 100\) strikes: \(\sim 12{,}800\) operations. BS formula: 100 strikes \(\times 20\) operations each \(= 2{,}000\) operations. COS is about \(6\times\) slower than BS for a single model, but COS works for any model with a known CF (Heston, VG, NIG, etc.) while BS is limited to lognormal. For non-GBM models, COS is the only viable analytical approach.