Skip to content

Commit b664ef9

Browse files
authored
Merge pull request #135 from PyFE/heston-mc
Ball & Roma (1994) implemented
2 parents 6835f19 + 0dd8564 commit b664ef9

File tree

8 files changed

+174
-62
lines changed

8 files changed

+174
-62
lines changed

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
include pyfeng/data/ousv_benchmark.xlsx
2+
include pyfeng/data/heston_benchmark.xlsx
13
include pyfeng/data/sabr_benchmark.xlsx

pyfeng/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
SabrChoiWu2021P,
1414
)
1515
from .garch import GarchMcTimeStep, GarchUncorrBaroneAdesi2004
16+
from .heston import HestonUncorrBallRoma1994
1617
from .ousv import OusvUncorrBallRoma1994
1718
from .sabr_int import SabrUncorrChoiWu2021
1819
from .sabr_mc import SabrMcCond

pyfeng/bsm.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,38 @@ def vega(self, strike, spot, texp, cp=1):
7070
sigma_std = np.maximum(self.sigma * np.sqrt(texp), np.finfo(float).eps)
7171
d1 = np.log(fwd / strike) / sigma_std + 0.5 * sigma_std
7272

73-
vega = (
74-
df * fwd * spst.norm.pdf(d1) * np.sqrt(texp)
75-
) # formula according to wikipedia
73+
# formula according to wikipedia
74+
vega = df * fwd * spst.norm.pdf(d1) * np.sqrt(texp)
7675
return vega
7776

77+
def d2_var(self, strike, spot, texp, cp=1):
78+
"""
79+
2nd derivative w.r.t. variance (=sigma^2)
80+
Eq. (9) in Hull & White (1987)
81+
82+
Args:
83+
strike: strike price
84+
spot: spot (or forward)
85+
texp: time to expiry
86+
cp: 1/-1 for call/put option
87+
88+
Returns: d^2 price / d var^2
89+
90+
References:
91+
- Hull J, White A (1987) The Pricing of Options on Assets with Stochastic Volatilities. The Journal of Finance 42:281–300. https://doi.org/10.1111/j.1540-6261.1987.tb02568.x
92+
"""
93+
fwd, df, _ = self._fwd_factor(spot, texp)
94+
95+
sigma_std = np.maximum(self.sigma * np.sqrt(texp), np.finfo(float).eps)
96+
d1 = np.log(fwd / strike) / sigma_std
97+
d2 = d1 - 0.5 * sigma_std
98+
d1 += 0.5 * sigma_std
99+
100+
d_sig2 = df * spot * np.sqrt(texp) * spst.norm.pdf(d1) * (d1*d2-1) / (4*self.sigma**3)
101+
102+
return d_sig2
103+
104+
78105
def delta(self, strike, spot, texp, cp=1):
79106

80107
fwd, df, divf = self._fwd_factor(spot, texp)

pyfeng/data/heston_benchmark.xlsx

2.2 KB
Binary file not shown.

pyfeng/garch.py

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,69 +6,75 @@
66

77
class GarchUncorrBaroneAdesi2004(sv.SvABC):
88
"""
9-
The implementation of Barone-Adesi et al (2004)'s approximation pricing formula for European
10-
options under uncorrelated (rho=0) GARCH diffusion model.
9+
Barone-Adesi et al (2004)'s approximation pricing formula for European options under uncorrelated (rho=0) GARCH diffusion model.
10+
Up to 2nd order is implemented.
1111
1212
References:
1313
- Barone-Adesi G, Rasmussen H, Ravanelli C (2005) An option pricing formula for the GARCH diffusion model. Computational Statistics & Data Analysis 49:287–310. https://doi.org/10.1016/j.csda.2004.05.014
1414
15-
This method is only used to compare with the method GarchCondMC.
15+
See Also: OusvUncorrBallRoma1994, HestonUncorrBallRoma1994
1616
"""
17+
1718
model_type = "GarchDiff"
19+
order = 2
1820

19-
def price(self, strike, spot, texp, cp=1):
21+
def avgvar_mv(self, var0, texp):
22+
"""
23+
Mean and variance of the average variance given V(0) = var0.
24+
Eqs. (12)-(13) in Barone-Adesi et al. (2005)
2025
21-
if not np.isclose(self.rho, 0.0):
22-
print(f"Pricing ignores rho = {self.rho}.")
26+
Args:
27+
var0: initial variance
28+
texp: time step
2329
24-
var0, mr, vov, theta = self.sigma, self.mr, self.vov, self.theta
30+
Returns:
31+
mean, variance
32+
"""
33+
34+
mr, vov, theta = self.mr, self.vov, self.theta
2535

2636
mr2 = mr * mr
2737
vov2 = vov * vov
2838
theta2 = theta*theta
29-
decay = np.exp(-mr * texp)
39+
mr_t = self.mr * texp
40+
e_mr = np.exp(-mr_t)
3041

3142
# Eq (12) of Barone-Adesi et al. (2005)
32-
M1 = theta + (var0 - theta) * (1 - decay) / (mr * texp)
43+
M1 = theta + (var0 - theta) * (1 - e_mr) / mr_t
3344

3445
term1 = vov2 - mr
3546
term2 = vov2 - 2*mr
3647

3748
# Eq (13)
38-
M2c_1 = - (decay * (var0 - theta))**2
49+
M2c_1 = -(e_mr * (var0 - theta))**2
3950
M2c_2 = 2*np.exp(term2 * texp) * (2*mr * theta * (mr * theta + term2 * var0) + term1 * term2 * var0**2)
40-
M2c_3 = -vov2 * (theta2 * (4*mr * (3 - texp * mr) + (2*texp * mr - 5) * vov2) + term2 * var0 * (2*theta + var0))
51+
M2c_3 = -vov2 * (theta2 * (4*mr * (3 - mr_t) + (2*mr_t - 5) * vov2) + term2 * var0 * (2*theta + var0))
4152

42-
M2c_4 = 2*decay * vov2
43-
M2c_4 *= 2*theta2 * (texp * mr2 - (1 + texp * mr) * vov2) \
53+
M2c_4 = 2*e_mr * vov2
54+
M2c_4 *= 2*theta2 * (mr_t * mr - (1 + mr_t) * vov2) \
4455
+ var0 * (2*mr * theta * (1 + texp * term1) + term1 * var0)
4556

4657
M2c = M2c_1 / mr2 + M2c_2 / (term1 * term2)**2 + M2c_3 / mr2 / term2**2 + M2c_4 / mr2 / term1**2
4758
M2c /= texp**2
48-
# M3c=None
49-
# M4c=None
5059

51-
# Eq. (11)
52-
logk = np.log(spot / strike)
53-
sigma_std = np.sqrt(self.sigma * texp)
60+
return M1, M2c
5461

55-
m = logk + (self.intr - self.divr) * texp
56-
d1 = logk / sigma_std + sigma_std / 2
62+
def price(self, strike, spot, texp, cp=1):
5763

58-
m_bs = bsm.Bsm(np.sqrt(M1), intr=self.intr, divr=self.divr)
59-
c_bs = m_bs.price(strike, spot, texp, cp)
64+
if not np.isclose(self.rho, 0.0):
65+
print(f"Pricing ignores rho = {self.rho}.")
6066

61-
c_bs_p1 = np.exp(-self.divr * texp) * strike * np.sqrt(texp / 4 / M1) * spst.norm.pdf(d1)
62-
c_bs_p2 = c_bs_p1 * texp / 2 * ((m / M1 / texp)**2 - 1 / M1 / texp - 1 / 4)
67+
avgvar, var = self.avgvar_mv(self.sigma, texp)
6368

64-
# C_bs_p3=C_bs_p1*(m**4/(4*(M1*texp)**4)-m**2*(12+M1*texp)/(8*(M1*texp)**3)+(48+8*M1*texp+(M1*texp)**2)/(64*(M1*texp)**2))*texp**2
65-
# C_bs_p4=C_bs_p1*(m**6/(8*(M1*texp)**6)-3*m**4*(20+M1*texp)/(32*(M1*texp)**5)+3*m**2*(240+24*M1*texp+(M1*texp)**2)/(128*(M1*texp)**4)-(960+144*M1*texp+12*(M1*texp)**2+(M1*texp)**3)/(512*(M1*texp)**3))*texp**3
69+
m_bs = bsm.Bsm(np.sqrt(avgvar), intr=self.intr, divr=self.divr)
70+
price = m_bs.price(strike, spot, texp, cp)
6671

67-
c_ga_2 = c_bs + (M2c / 2) * c_bs_p2
68-
# C_ga_3=C_ga_2+(M3c/6)*C_bs_p3
69-
# C_ga_4=C_ga_3+(M4c/24)*C_bs_p4
72+
if self.order == 2:
73+
price += 0.5 * var * m_bs.d2_var(strike, spot, texp, cp)
74+
elif self.order > 2:
75+
raise ValueError(f"Not implemented for approx order: {self.order}")
7076

71-
return c_ga_2
77+
return price
7278

7379

7480
class GarchMcTimeStep(sv.SvABC, sv.CondMcBsmABC):

pyfeng/heston.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import abc
2+
import numpy as np
3+
import scipy.stats as spst
4+
from . import sv_abc as sv
5+
from . import bsm
6+
7+
8+
class HestonABC(sv.SvABC, abc.ABC):
9+
model_type = "Heston"
10+
11+
def var_mv(self, var0, dt):
12+
"""
13+
Mean and variance of the variance V(t+dt) given V(0) = var_0
14+
15+
Args:
16+
var0: initial variance
17+
dt: time step
18+
19+
Returns:
20+
mean, variance
21+
"""
22+
23+
expo = np.exp(-self.mr * dt)
24+
m = self.theta + (var0 - self.theta)*expo
25+
s2 = var0*expo + self.theta*(1 - expo)/2
26+
s2 *= self.vov**2 * (1 - expo) / self.mr
27+
return m, s2
28+
29+
def avgvar_mv(self, var0, texp):
30+
"""
31+
Mean and variance of the average variance given V(0) = var_0.
32+
Appnedix B in Ball & Roma (1994)
33+
34+
Args:
35+
var0: initial variance
36+
texp: time step
37+
38+
Returns:
39+
mean, variance
40+
"""
41+
42+
mr_t = self.mr * texp
43+
e_mr = np.exp(-mr_t)
44+
x0 = var0 - self.theta
45+
vovn = self.vov * np.sqrt(texp) # normalized vov
46+
47+
m = self.theta + x0 * (1 - e_mr)/mr_t
48+
49+
var = (self.theta - 2*x0*e_mr) + \
50+
(var0 - 2.5*self.theta + (2*self.theta + (0.5*self.theta - var0)*e_mr)*e_mr)/mr_t
51+
var *= (vovn/mr_t)**2
52+
53+
return m, var
54+
55+
56+
class HestonUncorrBallRoma1994(HestonABC):
57+
"""
58+
Ball & Roma (1994)'s approximation pricing formula for European options under uncorrelated (rho=0) Heston model.
59+
Up to 2nd order is implemented.
60+
61+
See Also: OusvUncorrBallRoma1994, GarchUncorrBaroneAdesi2004
62+
"""
63+
64+
order = 2
65+
66+
def price(self, strike, spot, texp, cp=1):
67+
68+
if not np.isclose(self.rho, 0.0):
69+
print(f"Pricing ignores rho = {self.rho}.")
70+
71+
avgvar, var = self.avgvar_mv(self.sigma, texp)
72+
73+
m_bs = bsm.Bsm(np.sqrt(avgvar), intr=self.intr, divr=self.divr)
74+
price = m_bs.price(strike, spot, texp, cp)
75+
76+
if self.order == 2:
77+
price += 0.5 * var * m_bs.d2_var(strike, spot, texp, cp)
78+
elif self.order > 2:
79+
raise ValueError(f"Not implemented for approx order: {self.order}")
80+
81+
return price

pyfeng/heston_mc.py

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
from scipy.misc import derivative
99
import functools
1010
from . import sv_abc as sv
11+
from . import heston
1112

1213

13-
class HestonMcABC(sv.SvABC, sv.CondMcBsmABC, abc.ABC):
14+
class HestonMcABC(heston.HestonABC, sv.CondMcBsmABC, abc.ABC):
1415
var_process = True
15-
model_type = "Heston"
1616
scheme = None
1717

1818
def chi_dim(self):
@@ -41,24 +41,6 @@ def phi_exp(self, texp):
4141
phi = 4*self.mr / self.vov**2 / (1/exp - exp)
4242
return phi, exp
4343

44-
def var_mv(self, var_0, dt):
45-
"""
46-
Mean and variance of the variance V(t+dt) given V(0) = var_0
47-
48-
Args:
49-
var_0: initial variance
50-
dt: time step
51-
52-
Returns:
53-
mean, variance
54-
"""
55-
56-
expo = np.exp(-self.mr * dt)
57-
m = self.theta + (var_0 - self.theta) * expo
58-
s2 = var_0 * expo + self.theta * (1 - expo) / 2
59-
s2 *= self.vov**2 * (1 - expo) / self.mr
60-
return m, s2
61-
6244
def var_step_euler(self, var_0, dt, milstein=False):
6345
"""
6446
Simulate final variance with Euler/Milstein schemes (scheme = 0, 1)

pyfeng/ousv.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,25 @@
66

77

88
class OusvABC(sv.SvABC, abc.ABC):
9+
910
model_type = "OUSV"
1011

11-
def avgvar_mv(self, var_0, texp):
12+
def avgvar_mv(self, var0, texp):
1213
"""
1314
Mean and variance of the variance V(t+dt) given V(0) = var_0
1415
(variance is not implemented yet)
1516
1617
Args:
17-
var_0: initial variance
18+
var0: initial variance
1819
texp: time step
1920
2021
Returns:
21-
mean, variance(=NULL)
22+
mean, variance(=None)
2223
"""
2324

2425
mr_t = self.mr * texp
2526
e_mr = np.exp(-mr_t)
26-
x0 = var_0 - self.theta
27+
x0 = var0 - self.theta
2728
vv = self.vov**2/2/self.mr + self.theta**2 + \
2829
((x0**2 - self.vov**2/2/self.mr)*(1 + e_mr) + 4*self.theta * x0)*(1 - e_mr)/(2*self.mr*texp)
2930
return vv, None
@@ -129,16 +130,28 @@ def price(self, strike, spot, texp, cp=1):
129130

130131

131132
class OusvUncorrBallRoma1994(OusvABC):
133+
"""
134+
Ball & Roma (1994)'s approximation pricing formula for European options under uncorrelated (rho=0) OUSV model.
135+
Just 0th order (average variance) is implemented because higher order terms depends on numerical derivative of MGF
136+
137+
See Also: HestonUncorrBallRoma1994, GarchUncorrBaroneAdesi2004
138+
"""
139+
140+
order = 0
132141

133142
def price(self, strike, spot, texp, cp=1):
134143

135144
if not np.isclose(self.rho, 0.0):
136145
print(f"Pricing ignores rho = {self.rho}.")
137146

138147
avgvar, _ = self.avgvar_mv(self.sigma, texp)
148+
139149
m_bs = bsm.Bsm(np.sqrt(avgvar), intr=self.intr, divr=self.divr)
140150
price = m_bs.price(strike, spot, texp, cp)
141151

152+
if self.order > 0:
153+
raise ValueError(f"Not implemented for approx order: {self.order}")
154+
142155
return price
143156

144157

@@ -458,12 +471,12 @@ def cond_states(self, vol_0, texp):
458471
var_mean /= texp
459472
return vol_t, var_mean, vol_mean
460473

461-
def cond_states_step(self, vol_0, dt, zn=None):
474+
def cond_states_step(self, vol0, dt, zn=None):
462475
"""
463476
Incremental conditional states
464477
465478
Args:
466-
vol_0: initial volatility
479+
vol0: initial volatility
467480
dt: time step
468481
zn: specified normal rvs to use. (n_sin + 1, n_path)
469482
@@ -479,11 +492,11 @@ def cond_states_step(self, vol_0, dt, zn=None):
479492
cosh = np.cosh(mr_t)
480493
vovn = self.vov * np.sqrt(dt) # normalized vov
481494

482-
x_0 = vol_0 - self.theta
495+
x_0 = vol0 - self.theta
483496
if zn is None:
484-
x_t = self.vol_step(vol_0, dt) - self.theta
497+
x_t = self.vol_step(vol0, dt) - self.theta
485498
else:
486-
x_t = self.vol_step(vol_0, dt, zn=zn[0, :]) - self.theta
499+
x_t = self.vol_step(vol0, dt, zn=zn[0, :]) - self.theta
487500
sighat = x_t - x_0 * e_mr
488501

489502
if zn is None:

0 commit comments

Comments
 (0)