Skip to content

Commit 39265ec

Browse files
authored
Merge pull request #141 from PyFE/heston-mc
Various changes in the SV/SABR models.
2 parents 4e4d544 + 6550269 commit 39265ec

25 files changed

+1530
-1168
lines changed

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
nsvh
1616
sabr
1717
sabr_int
18+
sv_fft
1819
gamma
1920
multiasset
2021
multiasset_mc

docs/source/nsvh.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Hyperbolic Normal Stochastic Volatility (NSVh) Model
22
=======================================
33
.. automodule:: pyfeng.nsvh
4-
:members: Nsvh1
4+
:members: Nsvh1, NsvhQuadInt, NsvhMc
55
:noindex:

docs/source/sabr.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Stochastic-Alpha-Beta-Rho (SABR) Model
22
==========================
33
.. automodule:: pyfeng.sabr
4-
:members: SabrHagan2002, SabrLorig2017, SabrChoiWu2021H, SabrChoiWu2021P, SabrUncorrChoiWu2021
4+
:members: SabrHagan2002, SabrNormVolApprox, SabrLorig2017, SabrChoiWu2021H, SabrChoiWu2021P, SabrUncorrChoiWu2021
55
:inherited-members:
66
:exclude-members: SabrABC, SabrVolApproxABC
77
:noindex:

docs/source/sv_fft.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Stochastic volatility with Fourier inversion
2+
==========================
3+
.. automodule:: pyfeng.sv_fft
4+
:members: HestonFft, OusvFft
5+
:inherited-members:
6+
:exclude-members: FftABC
7+
:noindex:

pyfeng/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,21 @@
77
from .sv_fft import HestonFft, BsmFft, OusvFft, VarGammaFft, ExpNigFft
88
from .sabr import (
99
SabrHagan2002,
10-
SabrNorm,
10+
SabrNormVolApprox,
1111
SabrLorig2017,
1212
SabrChoiWu2021H,
1313
SabrChoiWu2021P,
1414
)
1515
from .garch import GarchMcTimeStep, GarchUncorrBaroneAdesi2004
1616
from .heston import HestonUncorrBallRoma1994
17+
from .heston_mc import (
18+
HestonMcAndersen2008, HestonMcGlassermanKim2011, HestonMcTseWan2013,
19+
HestonMcChoiKwok2023PoisGe, HestonMcChoiKwok2023PoisTd
20+
)
1721
from .ousv import OusvUncorrBallRoma1994
1822
from .sabr_int import SabrUncorrChoiWu2021
19-
from .sabr_mc import SabrMcCond
20-
from .nsvh import Nsvh1, NsvhMc
23+
from .sabr_mc import SabrMcTimeDisc
24+
from .nsvh import Nsvh1, NsvhMc, NsvhQuadInt
2125
from .multiasset import (
2226
BsmSpreadKirk,
2327
BsmSpreadBjerksund2014,

pyfeng/bsm.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,7 @@ def _impvol_newton(self, price, strike, spot, texp, cp=1, setval=False):
204204

205205
# Exclude optoin price below intrinsic value or above max value (1 for call or k for put)
206206
# ind_solve can be scalar or array. scalar can be fine in np.abs(p_err[ind_solve])
207-
ind_solve = (price_std - p_min > Bsm.IMPVOL_TOL) & (
208-
p_max - price_std > Bsm.IMPVOL_TOL
209-
)
207+
ind_solve = (price_std - p_min > Bsm.IMPVOL_TOL) & (p_max - price_std > Bsm.IMPVOL_TOL)
210208

211209
# initial guess = inflection point in sigma (volga=0)
212210
_sigma = np.ones_like(ind_solve)*np.sqrt(
@@ -287,21 +285,11 @@ def vol_smile(self, strike, spot, texp, cp=1, model="norm"):
287285
kk = strike/fwd
288286
lnk = np.log(kk)
289287
if model.lower() == "norm-approx":
290-
return (
291-
self.sigma
292-
*fwd
293-
*np.sqrt(kk)
294-
*(1 + lnk**2/24)
295-
/(1 + self.sigma**2*texp/24)
296-
)
288+
return self.sigma * fwd * np.sqrt(kk) * (1 + lnk**2/24) / (1 + self.sigma**2*texp/24)
297289
else:
298290
with np.errstate(divide="ignore", invalid="ignore"):
299291
term1 = np.where(np.fabs(lnk) > 1e-8, (kk - 1)/lnk, 2/(3 - kk))
300-
term2 = np.where(
301-
np.fabs(lnk) > 1e-8,
302-
(np.log(term1) - lnk/2)/lnk**2,
303-
1/24,
304-
)
292+
term2 = np.where(np.fabs(lnk) > 1e-8, (np.log(term1) - lnk/2)/lnk**2, 1/24)
305293
return self.sigma*fwd*term1*(1 - term2*self.sigma**2*texp)
306294
else:
307295
raise ValueError(f"Unknown model: {model}")

pyfeng/cev.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ def mass_zero(self, spot, texp, log=False):
4545

4646
betac = 1.0 - self.beta
4747
a = 0.5/betac
48-
sigma_std = np.maximum(
49-
self.sigma/np.power(fwd, betac)*np.sqrt(texp), np.finfo(float).eps
50-
)
48+
sigma_std = np.maximum(self.sigma/np.power(fwd, betac)*np.sqrt(texp), np.finfo(float).eps)
5149
x = 0.5/np.square(betac*sigma_std)
5250

5351
if log:

pyfeng/data/heston_benchmark.xlsx

-99 Bytes
Binary file not shown.

pyfeng/ex.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
#from .heston_fft import HestonFft
22

33
# SV models (CMC, AE) from ASP 2021
4-
from .heston_mc import HestonMcAndersen2008, HestonMcGlassermanKim2011, HestonMcTseWan2013, HestonMcChoiKwok2023
4+
from .heston_mixture import HestonMixture
55
from .sv32_mc import Sv32McCondQE, Sv32McAe2
66
from .sv32_mc2 import Sv32McTimeStep, Sv32McExactBaldeaux2012, Sv32McExactChoiKwok2023
77
from .subord_bm import VarGammaQuad, ExpNigQuad
88

99
# SABR / OUSV models for research
10-
from .sabr_int import SabrCondQuad
11-
from .sabr_mc import SabrMcExactCai2017
10+
from .sabr_int import SabrMixture
11+
from .sabr_mc import SabrMcCai2017Exact
1212
from .ousv import OusvSchobelZhu1998, OusvMcTimeStep, OusvMcChoi2023
1313

1414
# Basket-Asian from ASP 2021

pyfeng/garch.py

Lines changed: 53 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from . import sv_abc as sv
33
from . import bsm
44

5+
#### Use of RN generation spawn:
6+
# 0: simulation of variance (gamma/ncx2/normal)
57

68
class GarchUncorrBaroneAdesi2004(sv.SvABC):
79
"""
@@ -15,9 +17,10 @@ class GarchUncorrBaroneAdesi2004(sv.SvABC):
1517
"""
1618

1719
model_type = "GarchDiff"
20+
var_process = True
1821
order = 2
1922

20-
def avgvar_mv(self, var0, texp):
23+
def avgvar_mv(self, texp, var0):
2124
"""
2225
Mean and variance of the average variance given V(0) = var0.
2326
Eqs. (12)-(13) in Barone-Adesi et al. (2005)
@@ -63,7 +66,7 @@ def price(self, strike, spot, texp, cp=1):
6366
if not np.isclose(self.rho, 0.0):
6467
print(f"Pricing ignores rho = {self.rho}.")
6568

66-
avgvar, var = self.avgvar_mv(self.sigma, texp)
69+
avgvar, var = self.avgvar_mv(texp, self.sigma)
6770

6871
m_bs = bsm.Bsm(np.sqrt(avgvar), intr=self.intr, divr=self.divr)
6972
price = m_bs.price(strike, spot, texp, cp)
@@ -103,7 +106,7 @@ def set_num_params(self, n_path=10000, dt=0.05, rn_seed=None, antithetic=True, s
103106
super().set_num_params(n_path, dt, rn_seed, antithetic)
104107
self.scheme = scheme
105108

106-
def var_step_euler(self, var_0, dt, milstein=True):
109+
def vol_step_euler(self, dt, var_0, milstein=True):
107110
"""
108111
Euler/Milstein Schemes:
109112
v_(t+dt) = v_t + mr * (theta - v_t) * dt + vov * v_t Z * sqrt(dt) + (vov^2/2) v_t (Z^2-1) dt
@@ -115,8 +118,7 @@ def var_step_euler(self, var_0, dt, milstein=True):
115118
Returns: Variance path (time, path) including the value at t=0
116119
"""
117120

118-
zz = self.rv_normal()
119-
121+
zz = self.rv_normal(spawn=0)
120122
var_t = var_0 + self.mr*(self.theta - var_0)*dt + self.vov*var_0*np.sqrt(dt)*zz
121123
if milstein:
122124
var_t += (self.vov**2/2)*var_0*dt*(zz**2 - 1)
@@ -138,64 +140,69 @@ def var_step_log(self, log_var_0, dt):
138140
Returns: Variance path (time, path) including the value at t=0
139141
"""
140142

141-
zz = self.rv_normal()
143+
zz = self.rv_normal(spawn=0)
142144

143145
log_var_t = log_var_0 + (self.mr*self.theta*np.exp(-log_var_0) - self.mr - self.vov**2/2)*dt \
144146
+ self.vov*np.sqrt(dt)*zz
145147

146148
return log_var_t
147149

148-
def cond_states(self, var_0, texp):
149-
tobs = self.tobs(texp)
150-
n_dt = len(tobs)
151-
dt = np.diff(tobs, prepend=0)
152-
153-
# precalculate the Simpson's rule weight
154-
weight = np.ones(n_dt + 1)
155-
weight[1:-1:2] = 4
156-
weight[2:-1:2] = 2
157-
weight /= weight.sum()
158-
159-
var_t = np.full(self.n_path, var_0)
160-
161-
mean_var = weight[0]*var_t
162-
mean_vol = weight[0]*np.sqrt(var_t)
163-
mean_inv_vol = weight[0]/np.sqrt(var_t)
150+
def cond_states_step(self, dt, var_0):
164151

165152
if self.scheme < 2:
166153
milstein = (self.scheme == 1)
154+
var_t = self.vol_step_euler(dt, var_0, milstein=milstein)
167155

168-
for i in range(n_dt):
169-
var_t = self.var_step_euler(var_t, dt[i], milstein=milstein)
170-
mean_var += weight[i + 1]*var_t
171-
vol_t = np.sqrt(var_t)
172-
mean_vol += weight[i + 1]*vol_t
173-
mean_inv_vol += weight[i + 1]/vol_t
156+
mean_var = (var_0 + var_t)/2
157+
vol_0 = np.sqrt(var_0)
158+
vol_t = np.sqrt(var_t)
159+
mean_vol = (vol_0 + vol_t)/2
160+
mean_inv_vol = (1/vol_0 + 1/vol_t)/2
174161

175162
elif self.scheme == 2:
176-
log_var_t = np.full(self.n_path, np.log(var_0))
177-
for i in range(n_dt):
178-
# Euler scheme on Log(var)
179-
log_var_t = self.var_step_log(log_var_t, dt[i])
180-
vol_t = np.exp(log_var_t/2)
181-
mean_var += weight[i + 1]*vol_t**2
182-
mean_vol += weight[i + 1]*vol_t
183-
mean_inv_vol += weight[i + 1]/vol_t
163+
164+
log_var_t = self.var_step_log(np.log(var_0), dt)
165+
166+
vol_0 = np.sqrt(var_0)
167+
vol_t = np.exp(log_var_t/2)
168+
var_t = vol_t**2
169+
170+
mean_var = (var_0 + var_t)/2
171+
mean_vol = (vol_0 + vol_t)/2
172+
mean_inv_vol = (1/vol_0 + 1/vol_t)/2
184173
else:
185174
raise ValueError(f'Invalid scheme: {self.scheme}')
186175

187-
return vol_t, mean_var, mean_vol, mean_inv_vol
188-
189-
def cond_spot_sigma(self, var_0, texp):
176+
return var_t, mean_var, mean_vol, mean_inv_vol
190177

191-
vol_final, mean_var, mean_vol, mean_inv_vol = self.cond_states(var_0, texp)
178+
def cond_spot_sigma(self, texp, var_0):
179+
tobs = self.tobs(texp)
180+
dt = np.diff(tobs, prepend=0)
181+
n_dt = len(dt)
192182

193-
spot_cond = 2*(vol_final - np.sqrt(var_0))/self.vov \
194-
- self.mr*self.theta*mean_inv_vol*texp/self.vov \
195-
+ (self.mr/self.vov + self.vov/4)*mean_vol*texp \
196-
- self.rho*mean_var*texp/2
183+
var_t = np.full(self.n_path, var_0)
184+
avgvar = np.zeros(self.n_path)
185+
avgvol = np.zeros(self.n_path)
186+
avgivol = np.zeros(self.n_path)
187+
188+
for i in range(n_dt):
189+
var_t, avgvar_inc, avgvol_inc, avgivol_inc = self.cond_states_step(dt[i], var_t)
190+
avgvar += avgvar_inc * dt[i]
191+
avgvol += avgvol_inc * dt[i]
192+
avgivol += avgivol_inc * dt[i]
193+
194+
avgvar /= texp
195+
avgvol /= texp
196+
avgivol /= texp
197+
198+
spot_cond = 2 * (np.sqrt(var_t) - np.sqrt(var_0)) / self.vov + \
199+
(-self.mr * self.theta * avgivol / self.vov \
200+
+ (self.mr/self.vov + self.vov/4) * avgvol - self.rho * avgvar / 2) * texp
197201
np.exp(self.rho*spot_cond, out=spot_cond)
198202

199-
sigma_cond = np.sqrt((1.0 - self.rho**2)/var_0*mean_var)
203+
cond_sigma = np.sqrt((1.0 - self.rho**2)/var_0*avgvar)
204+
205+
return spot_cond, cond_sigma
200206

201-
return spot_cond, sigma_cond
207+
def return_var_realized(self, texp, cond):
208+
return None

0 commit comments

Comments
 (0)