Skip to content

Commit c1ab23c

Browse files
authored
Merge pull request #142 from PyFE/ousv
Cumulative changes on SV models
2 parents 32f7c37 + 2988c69 commit c1ab23c

17 files changed

+667
-727
lines changed

pyfeng/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .bsm import Bsm, BsmDisp
55
from .cev import Cev
66
from .gamma import InvGam, InvGauss
7-
from .sv_fft import HestonFft, BsmFft, OusvFft, VarGammaFft, ExpNigFft
7+
from .sv_fft import HestonFft, BsmFft, OusvFft, VarGammaFft, ExpNigFft, Sv32Fft
88
from .sabr import (
99
SabrHagan2002,
1010
SabrNormVolApprox,
@@ -21,7 +21,7 @@
2121
from .ousv import OusvUncorrBallRoma1994
2222
from .sabr_int import SabrUncorrChoiWu2021
2323
from .sabr_mc import SabrMcTimeDisc
24-
from .nsvh import Nsvh1, NsvhMc, NsvhQuadInt
24+
from .nsvh import Nsvh1, NsvhMc, NsvhGaussQuad
2525
from .multiasset import (
2626
BsmSpreadKirk,
2727
BsmSpreadBjerksund2014,

pyfeng/data/sabr_benchmark.xlsx

4.43 KB
Binary file not shown.

pyfeng/ex.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
# SV models (CMC, AE) from ASP 2021
44
from .heston_mixture import HestonMixture
5-
from .sv32_mc import Sv32McCondQE, Sv32McAe2
6-
from .sv32_mc2 import Sv32McTimeStep, Sv32McExactBaldeaux2012, Sv32McExactChoiKwok2023
5+
from .sv32_mc2 import Sv32McTimeStep, Sv32McBaldeaux2012Exact, Sv32McChoiKwok2023Ig
76
from .subord_bm import VarGammaQuad, ExpNigQuad
87

98
# SABR / OUSV models for research
109
from .sabr_int import SabrMixture
1110
from .sabr_mc import SabrMcCai2017Exact
12-
from .ousv import OusvSchobelZhu1998, OusvMcTimeStep, OusvMcChoi2023
11+
from .ousv import OusvSchobelZhu1998, OusvMcTimeStep, OusvMcChoi2023KL
1312

1413
# Basket-Asian from ASP 2021
1514
from .multiasset_Ju2002 import BsmBasketAsianJu2002, BsmContinuousAsianJu2002

pyfeng/heston.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def strike_var_swap_analytic(self, texp, dt):
8383
8484
Args:
8585
texp: time to expiry
86-
dt: observation time step. If None, continuous monitoring
86+
dt: observation time step. If zero, continuous monitoring
8787
8888
Returns:
8989
Fair strike
@@ -100,7 +100,7 @@ def strike_var_swap_analytic(self, texp, dt):
100100
x0 = var0 - self.theta
101101
strike = self.theta + x0*(1 - e_mr_t)/mr_t
102102

103-
if dt is not None:
103+
if not np.all(np.isclose(dt, 0.0)):
104104
### adjustment for discrete monitoring
105105
mr_h = self.mr * dt
106106
e_mr_h = np.exp(-mr_h)

pyfeng/heston_mc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ def cond_spot_sigma(self, texp, var_0):
169169
return spot_cond, sigma_cond
170170

171171
def strike_var_swap_analytic(self, texp, dt=None):
172-
dt = dt or self.dt
172+
if dt is None:
173+
dt = self.dt
173174
rv = super().strike_var_swap_analytic(texp, dt)
174175
return rv
175176

pyfeng/nsvh.py

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ def price(self, strike, spot, texp, cp=1):
309309
return df * price
310310

311311

312-
class NsvhQuadInt(sabr.SabrABC):
312+
class NsvhGaussQuad(sabr.SabrABC):
313313
"""
314314
Quadrature integration method of Hyperbolic Normal Stochastic Volatility (NSVh) model.
315315
@@ -325,22 +325,22 @@ class NsvhQuadInt(sabr.SabrABC):
325325
>>> import numpy as np
326326
>>> import pyfeng as pf
327327
>>> #### Nsvh1: comparison with analytic pricing
328-
>>> m1 = pf.NsvhQuadInt(sigma=20, vov=0.8, rho=-0.3, lam=1.0)
328+
>>> m1 = pf.NsvhGaussQuad(sigma=20, vov=0.8, rho=-0.3, lam=1.0)
329329
>>> m2 = pf.Nsvh1(sigma=20, vov=0.8, rho=-0.3)
330330
>>> p1 = m1.price(np.arange(80, 121, 10), 100, 1.2)
331331
>>> p2 = m2.price(np.arange(80, 121, 10), 100, 1.2)
332332
>>> p1 - p2
333333
array([0.00345526, 0.00630649, 0.00966333, 0.00571175, 0.00017924])
334334
>>> #### Normal SABR: comparison with vol approximation
335-
>>> m1 = pf.NsvhQuadInt(sigma=20, vov=0.8, rho=-0.3, lam=0.0)
335+
>>> m1 = pf.NsvhGaussQuad(sigma=20, vov=0.8, rho=-0.3, lam=0.0)
336336
>>> m2 = pf.SabrNormVolApprox(sigma=20, vov=0.8, rho=-0.3)
337337
>>> p1 = m1.price(np.arange(80, 121, 10), 100, 1.2)
338338
>>> p2 = m2.price(np.arange(80, 121, 10), 100, 1.2)
339339
>>> p1 - p2
340340
array([-0.17262802, -0.10160687, -0.00802731, 0.0338126 , 0.01598512])
341341
342342
References:
343-
Choi J (2023), Unpublished Working Paper.
343+
Choi J (2023), Option pricing under the normal SABR model with Gaussian quadratures. Unpublished Working Paper.
344344
345345
"""
346346

@@ -372,16 +372,13 @@ def price(self, strike, spot, texp, cp=1):
372372
### axis 1: nodes of x,y,z , get the weight of z,v
373373
z_value, z_weight = spsp.roots_hermitenorm(self.n_quad[0])
374374
z_weight /= np.sqrt(2 * np.pi)
375+
z_weight /= np.sum(np.exp(vovn/2*z_value)*z_weight) / np.exp(vovn**2/8)
376+
#print(np.sum(np.exp(-vovn/2*z_value)*z_weight) - np.exp(vovn**2/8))
375377

376-
if self.n_quad[1] is not None:
377-
# quadrature point & weight for exp(-v) derived from sqrt(v) * np.exp(-v/2)
378-
v_value, v_weight = spsp.roots_genlaguerre(self.n_quad[1], 0.5)
379-
v_weight *= 2 / np.sqrt(v_value)
380-
v_value *= 2.0
381-
else:
382-
# uniform grid from v=0 to 40 from np.exp(-20) ~ 2e-9
383-
v_value = np.arange(1, 8001) / 200
384-
v_weight = np.full_like(v_value, 1 / 200) * np.exp(-v_value/2)
378+
# quadrature point & weight for exp(-v/2)/(2 pi) derived from sqrt(v) * np.exp(-v)
379+
v_value, v_weight = spsp.roots_genlaguerre(self.n_quad[1], 0.5)
380+
v_weight /= np.pi * np.sqrt(v_value)
381+
v_value *= 2.0
385382

386383
### axis 0: dependence on v
387384
v_value = v_value[:, None]
@@ -408,7 +405,8 @@ def price(self, strike, spot, texp, cp=1):
408405
theta_mat = np.arccos(np.abs(g_vec) / h_mat)
409406

410407
int_z_v = np.sqrt(h_mat**2 - g_vec**2) - np.abs(g_vec) * theta_mat
411-
int_z = np.sum(int_z_v * v_weight, axis=0) / (2*np.pi) # integrating over v (column)
408+
409+
int_z = np.sum(int_z_v * v_weight, axis=0) # integrating over v (column)
412410
int_z[:] = int_z * np.exp(-v_0/2) + np.fmax(cp[i] * g_vec, 0.0) # in-place operation
413411

414412
price[i] = np.sum(int_z * z_weight)
@@ -419,3 +417,52 @@ def price(self, strike, spot, texp, cp=1):
419417
price = price[0]
420418

421419
return price
420+
421+
def cdf(self, strike, spot, texp, cp=-1):
422+
fwd = self.forward(spot, texp)
423+
_, _, rhoc, rho2, vovn = self._variables(1.0, texp)
424+
425+
### axis 1: nodes of x,y,z , get the weight of z,v
426+
z_value, z_weight = spsp.roots_hermitenorm(self.n_quad[0])
427+
z_weight /= np.sqrt(2 * np.pi)
428+
z_weight /= np.sum(np.exp(vovn/2*z_value)*z_weight) / np.exp(vovn**2/8)
429+
430+
# quadrature point & weight for exp(-v/2)/(2 pi) derived from np.exp(-v)
431+
v_value, v_weight = spsp.roots_genlaguerre(self.n_quad[1], 0.0)
432+
v_weight /= np.pi
433+
v_value *= 2
434+
435+
### axis 0: dependence on v
436+
v_value = v_value[:, None]
437+
v_weight = v_weight[:, None]
438+
439+
vov_var = np.exp(0.5 * self.lam * vovn ** 2)
440+
441+
#### effective strike
442+
strike_eff = (self.vov / self.sigma) * (strike - fwd)
443+
scalar_output = np.isscalar(strike_eff)
444+
445+
strike_eff, cp = np.broadcast_arrays(np.atleast_1d(strike_eff), cp)
446+
447+
u_hat = (z_value + 0.5 * self.lam * vovn) # column (z direction)
448+
exp_plus = np.exp(vovn * u_hat / 2)
449+
z_star_cosh = (exp_plus ** 2 + 1 / exp_plus ** 2) / 2
450+
cdf = np.zeros_like(strike, dtype=float)
451+
452+
for i, k_eff in enumerate(strike_eff):
453+
g_vec = self.rho * exp_plus - (self.rho * vov_var + k_eff) / exp_plus
454+
temp1 = z_star_cosh + 0.5 * g_vec ** 2 / (1 - rho2)
455+
v_0 = (np.arccosh(temp1) / vovn) ** 2 - u_hat ** 2
456+
h_mat = rhoc * np.sqrt(
457+
2 * np.cosh(vovn * np.sqrt((u_hat ** 2 + v_0 + v_value))) - 2 * np.cosh(vovn * u_hat))
458+
theta_mat = np.arccos(np.abs(g_vec) / h_mat)
459+
460+
int_z = np.sum(theta_mat * np.exp(-v_0/2) * v_weight, axis=0) # integrating over v (column)
461+
int_z[cp[i]*g_vec > 0] = 1.0 - int_z[cp[i]*g_vec > 0]
462+
int_z *= np.exp(-z_value*vovn/2 - vovn**2/8)
463+
cdf[i] = np.sum(int_z * z_weight)
464+
465+
if scalar_output:
466+
cdf = cdf[0]
467+
468+
return cdf

pyfeng/opt_abc.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ def theta_numeric(self, strike, spot, texp, cp=1):
343343

344344
def pdf_numeric(self, strike, spot, texp, cp=-1, h=0.001):
345345
"""
346-
Probability density functin (PDF) at `strike`
346+
Probability density function (PDF) at `strike`
347347
348348
Args:
349349
strike: strike price
@@ -352,16 +352,37 @@ def pdf_numeric(self, strike, spot, texp, cp=-1, h=0.001):
352352
cp: 1/-1 for call/put
353353
354354
Returns:
355-
probability densitiy
355+
PDF values
356356
"""
357-
fwd = spot * (1.0 if self.is_fwd else np.exp(texp * (self.intr - self.divr)))
357+
fwd = self.forward(spot, texp)
358358
kk = strike / fwd
359359
kk_arr = np.array([kk - h, kk, kk + h]).flatten()
360-
price = self.price(kk_arr, 1, texp, cp=cp)
360+
price = self.price(kk_arr, 1.0, texp, cp=cp)
361361
price = price.reshape(3, -1)
362362
pdf = (price[2] + price[0] - 2.0 * price[1]) / (h * h)
363363
return pdf
364364

365+
def cdf_numeric(self, strike, spot, texp, cp=-1, h=0.001):
366+
"""
367+
Cumulative distribution function (CDF) at `strike`
368+
369+
Args:
370+
strike: strike price
371+
spot: spot price
372+
texp: time to expiry
373+
cp: 1/-1 for call/put
374+
375+
Returns:
376+
CDF values
377+
"""
378+
fwd = self.forward(spot, texp)
379+
#kk = strike / fwd
380+
strike_arr = np.array([strike - h, strike + h]).flatten()
381+
price = self.price(strike_arr, fwd, texp, cp=cp)
382+
price = price.reshape(2, -1)
383+
cdf = np.sign(cp)*(price[0] - price[1]) / (2*h)
384+
return cdf
385+
365386
# create aliases
366387
delta = delta_numeric
367388
gamma = gamma_numeric

0 commit comments

Comments
 (0)