Skip to content

Commit 6864bd4

Browse files
committed
Refactor Andersen class. Started avgvar_var_unexplained
1 parent f8de3a9 commit 6864bd4

File tree

1 file changed

+196
-151
lines changed

1 file changed

+196
-151
lines changed

pyfeng/heston_mc.py

+196-151
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ def chi_dim(self):
2525
chi_dim = 4 * self.theta * self.mr / self.vov**2
2626
return chi_dim
2727

28-
def chi_lambda(self, df):
28+
def chi_lambda(self, dt):
2929
"""
3030
Noncentral Chi-square (NCX) distribution's noncentrality parameter
3131
3232
Returns:
3333
noncentrality parameter (scalar)
3434
"""
3535
chi_lambda = 4 * self.sigma * self.mr / self.vov**2
36-
chi_lambda /= np.exp(self.mr * df) - 1
36+
chi_lambda /= np.exp(self.mr*dt) - 1
3737
return chi_lambda
3838

3939
def phi_exp(self, texp):
@@ -117,6 +117,14 @@ def cond_states(self, var_0, dt):
117117
def cond_spot_sigma(self, var_0, texp):
118118
var_final, var_avg = self.cond_states(var_0, texp)
119119

120+
avgvar_m, avgvar_v = self.avgvar_mv(var_0, texp)
121+
self.result = {**self.result,
122+
'avgvar mean': avgvar_m,
123+
'avgvar mean error': var_avg.mean()/avgvar_m - 1,
124+
'avgvar var': avgvar_v,
125+
'avgvar var error': np.square(var_avg - avgvar_m).mean()/avgvar_v - 1
126+
}
127+
120128
spot_cond = ((var_final - var_0) - self.mr * texp * (self.theta - var_avg)) / self.vov \
121129
- 0.5 * self.rho * var_avg * texp
122130
np.exp(self.rho * spot_cond, out=spot_cond)
@@ -126,154 +134,6 @@ def cond_spot_sigma(self, var_0, texp):
126134
return spot_cond, sigma_cond
127135

128136

129-
class HestonMcAndersen2008(HestonMcABC):
130-
"""
131-
Heston model with conditional Monte-Carlo simulation
132-
133-
Conditional MC for Heston model based on QE discretization scheme by Andersen (2008).
134-
135-
Underlying price follows a geometric Brownian motion, and variance of the price follows a CIR process.
136-
137-
References:
138-
- Andersen L (2008) Simple and efficient simulation of the Heston stochastic volatility model. Journal of Computational Finance 11:1–42. https://doi.org/10.21314/JCF.2008.189
139-
140-
Examples:
141-
>>> import numpy as np
142-
>>> import pyfeng.ex as pfex
143-
>>> strike = np.array([60, 100, 140])
144-
>>> spot = 100
145-
>>> sigma, vov, mr, rho, texp = 0.04, 1, 0.5, -0.9, 10
146-
>>> m = pfex.HestonMcAndersen2008(sigma, vov=vov, mr=mr, rho=rho)
147-
>>> m.set_num_params(n_path=1e5, dt=1/8, rn_seed=123456)
148-
>>> m.price(strike, spot, texp)
149-
>>> # true price: 44.330, 13.085, 0.296
150-
array([44.31943535, 13.09371251, 0.29580431])
151-
"""
152-
psi_c = 1.5 # parameter used by the Andersen QE scheme
153-
scheme = 4
154-
155-
def set_num_params(self, n_path=10000, dt=0.05, rn_seed=None, antithetic=True, scheme=4):
156-
"""
157-
Set MC parameters
158-
159-
Args:
160-
n_path: number of paths
161-
dt: time step for Euler/Milstein steps
162-
rn_seed: random number seed
163-
antithetic: antithetic
164-
scheme: 0 for Euler, 1 for Milstein, 2 for NCX2, 3 for Poisson-mixture Gamma, 4 for Andersen (2008)'s QE scheme
165-
166-
References:
167-
- Andersen L (2008) Simple and efficient simulation of the Heston stochastic volatility model. Journal of Computational Finance 11:1–42. https://doi.org/10.21314/JCF.2008.189
168-
"""
169-
super().set_num_params(n_path, dt, rn_seed, antithetic)
170-
self.scheme = scheme
171-
172-
def var_step_qe(self, var_0, dt):
173-
m, s2 = self.var_mv(var_0, dt)
174-
psi = s2 / m**2
175-
176-
zz = self.rv_normal(spawn=0)
177-
178-
# compute vt(i+1) given psi
179-
# psi < psi_c
180-
idx_below = (psi <= self.psi_c)
181-
ins = 2 / psi[idx_below]
182-
b2 = (ins - 1) + np.sqrt(ins * (ins - 1))
183-
a = m[idx_below] / (1 + b2)
184-
185-
var_t = np.zeros(self.n_path)
186-
var_t[idx_below] = a * (np.sqrt(b2) + zz[idx_below])**2
187-
188-
# psi_c < psi
189-
one_m_u = spst.norm.cdf(zz[~idx_below]) # 1 - U
190-
var_t_above = np.zeros_like(one_m_u)
191-
192-
one_m_p = 2 / (psi[~idx_below] + 1) # 1 - p
193-
beta = one_m_p / m[~idx_below]
194-
195-
# No need to consider (uu <= pp) & ~idx_below because the var_t value will be zero
196-
idx_above = (one_m_u <= one_m_p)
197-
var_t_above[idx_above] = (np.log(one_m_p / one_m_u) / beta)[idx_above]
198-
199-
var_t[~idx_below] = var_t_above
200-
201-
return var_t
202-
203-
def vol_paths(self, tobs):
204-
var_0 = self.sigma
205-
dt = np.diff(tobs, prepend=0)
206-
n_dt = len(dt)
207-
208-
var_path = np.full((n_dt + 1, self.n_path), var_0) # variance series: V0, V1,...,VT
209-
var_t = np.full(self.n_path, var_0)
210-
211-
if self.scheme < 2:
212-
milstein = (self.scheme == 1)
213-
for i in range(n_dt):
214-
# Euler (or Milstein) scheme
215-
var_t = self.var_step_euler(var_t, dt[i], milstein=milstein)
216-
var_path[i + 1, :] = var_t
217-
218-
elif self.scheme == 2:
219-
for i in range(n_dt):
220-
var_t = self.var_step_ncx2(var_t, dt[i])
221-
var_path[i + 1, :] = var_t
222-
223-
elif self.scheme == 3:
224-
for i in range(n_dt):
225-
var_t, _ = self.var_step_ncx2_eta(var_t, dt[i])
226-
var_path[i + 1, :] = var_t
227-
228-
elif self.scheme == 4:
229-
for i in range(n_dt):
230-
var_t = self.var_step_qe(var_t, dt[i])
231-
var_path[i + 1, :] = var_t
232-
233-
else:
234-
raise ValueError(f'Invalid scheme: {self.scheme}')
235-
236-
return var_path
237-
238-
def cond_states(self, var_0, texp):
239-
240-
tobs = self.tobs(texp)
241-
n_dt = len(tobs)
242-
dt = np.diff(tobs, prepend=0)
243-
244-
# precalculate the Simpson's rule weight
245-
weight = np.ones(n_dt + 1)
246-
weight[1:-1] = 2
247-
weight /= weight.sum()
248-
249-
var_t = np.full(self.n_path, var_0)
250-
var_avg = weight[0] * var_t
251-
252-
if self.scheme < 2:
253-
milstein = (self.scheme == 1)
254-
for i in range(n_dt):
255-
# Euler (or Milstein) scheme
256-
var_t = self.var_step_euler(var_t, dt[i], milstein=milstein)
257-
var_avg += weight[i + 1] * var_t
258-
259-
elif self.scheme == 2:
260-
for i in range(n_dt):
261-
var_t = self.var_step_ncx2(var_t, dt[i])
262-
var_avg += weight[i + 1] * var_t
263-
264-
elif self.scheme == 3:
265-
for i in range(n_dt):
266-
var_t, _ = self.var_step_ncx2_eta(var_t, dt[i])
267-
var_avg += weight[i + 1] * var_t
268-
269-
elif self.scheme == 4:
270-
for i in range(n_dt):
271-
var_t = self.var_step_qe(var_t, dt[i])
272-
var_avg += weight[i + 1] * var_t
273-
274-
return var_t, var_avg # * texp
275-
276-
277137
class HestonMcGlassermanKim2011(HestonMcABC):
278138
"""
279139
Exact simulation using the gamma series based on Glasserman & Kim (2011)
@@ -797,6 +657,192 @@ def cond_states(self, var_0, texp):
797657
return var_t, var_avg
798658

799659

660+
class HestonMcAndersen2008(HestonMcGlassermanKim2011):
661+
"""
662+
Heston model with conditional Monte-Carlo simulation
663+
664+
Conditional MC for Heston model based on QE discretization scheme by Andersen (2008).
665+
666+
Underlying price follows a geometric Brownian motion, and variance of the price follows a CIR process.
667+
668+
References:
669+
- Andersen L (2008) Simple and efficient simulation of the Heston stochastic volatility model. Journal of Computational Finance 11:1–42. https://doi.org/10.21314/JCF.2008.189
670+
671+
Examples:
672+
>>> import numpy as np
673+
>>> import pyfeng.ex as pfex
674+
>>> strike = np.array([60, 100, 140])
675+
>>> spot = 100
676+
>>> sigma, vov, mr, rho, texp = 0.04, 1, 0.5, -0.9, 10
677+
>>> m = pfex.HestonMcAndersen2008(sigma, vov=vov, mr=mr, rho=rho)
678+
>>> m.set_num_params(n_path=1e5, dt=1/8, rn_seed=123456)
679+
>>> m.price(strike, spot, texp)
680+
>>> # true price: 44.330, 13.085, 0.296
681+
array([44.31943535, 13.09371251, 0.29580431])
682+
"""
683+
psi_c = 1.5 # parameter used by the Andersen QE scheme
684+
scheme = 4
685+
686+
def set_num_params(self, n_path=10000, dt=0.05, rn_seed=None, antithetic=True, scheme=4):
687+
"""
688+
Set MC parameters
689+
690+
Args:
691+
n_path: number of paths
692+
dt: time step for Euler/Milstein steps
693+
rn_seed: random number seed
694+
antithetic: antithetic
695+
scheme: 0 for Euler, 1 for Milstein, 2 for NCX2, 3 for Poisson-mixture Gamma, 4 for Andersen (2008)'s QE scheme
696+
697+
References:
698+
- Andersen L (2008) Simple and efficient simulation of the Heston stochastic volatility model. Journal of Computational Finance 11:1–42. https://doi.org/10.21314/JCF.2008.189
699+
"""
700+
super().set_num_params(n_path, dt, rn_seed, antithetic)
701+
self.scheme = scheme
702+
703+
def var_step_qe(self, var_0, dt):
704+
m, psi = self.var_mv(var_0, dt) # put variance into psi
705+
psi /= m**2
706+
707+
zz = self.rv_normal(spawn=0)
708+
709+
# compute vt(i+1) given psi
710+
# psi < psi_c
711+
idx_below = (psi <= self.psi_c)
712+
ins = 2 / psi[idx_below]
713+
b2 = (ins - 1) + np.sqrt(ins * (ins - 1)) # b^2. Eq (27)
714+
a = m[idx_below] / (1 + b2) # Eq (28)
715+
716+
var_t = np.zeros(self.n_path)
717+
var_t[idx_below] = a * (np.sqrt(b2) + zz[idx_below])**2 # Eq (23)
718+
719+
# psi_c < psi
720+
one_m_u = spst.norm.cdf(zz[~idx_below]) # 1 - U
721+
var_t_above = np.zeros_like(one_m_u)
722+
723+
one_m_p = 2 / (psi[~idx_below] + 1) # 1 - p. Eq (29)
724+
beta = one_m_p / m[~idx_below] # Eq (30)
725+
726+
# No need to consider (uu <= pp) & ~idx_below because the var_t value will be zero
727+
idx_above = (one_m_u <= one_m_p)
728+
var_t_above[idx_above] = (np.log(one_m_p / one_m_u) / beta)[idx_above] # Eq (25)
729+
730+
var_t[~idx_below] = var_t_above
731+
732+
return var_t
733+
734+
def vol_paths(self, tobs):
735+
var_0 = self.sigma
736+
dt = np.diff(tobs, prepend=0)
737+
n_dt = len(dt)
738+
739+
var_path = np.full((n_dt + 1, self.n_path), var_0) # variance series: V0, V1,...,VT
740+
var_t = np.full(self.n_path, var_0)
741+
742+
if self.scheme < 2:
743+
milstein = (self.scheme == 1)
744+
for i in range(n_dt):
745+
# Euler (or Milstein) scheme
746+
var_t = self.var_step_euler(var_t, dt[i], milstein=milstein)
747+
var_path[i + 1, :] = var_t
748+
749+
elif self.scheme == 2:
750+
for i in range(n_dt):
751+
var_t = self.var_step_ncx2(var_t, dt[i])
752+
var_path[i + 1, :] = var_t
753+
754+
elif self.scheme == 3:
755+
for i in range(n_dt):
756+
var_t, _ = self.var_step_ncx2_eta(var_t, dt[i])
757+
var_path[i + 1, :] = var_t
758+
759+
elif self.scheme == 4:
760+
for i in range(n_dt):
761+
var_t = self.var_step_qe(var_t, dt[i])
762+
var_path[i + 1, :] = var_t
763+
764+
else:
765+
raise ValueError(f'Invalid scheme: {self.scheme}')
766+
767+
return var_path
768+
769+
def cond_states(self, var_0, texp):
770+
771+
tobs = self.tobs(texp)
772+
n_dt = len(tobs)
773+
dt = np.diff(tobs, prepend=0)
774+
775+
# precalculate the Trapezoidal rule weight
776+
weight = np.full(n_dt + 1, 1/n_dt)
777+
weight[[0, -1]] = 0.5/n_dt # the first and last element
778+
779+
var_t = np.full(self.n_path, var_0)
780+
781+
if self.scheme < 2:
782+
milstein = (self.scheme == 1)
783+
var_avg = weight[0]*var_t
784+
for i in range(n_dt):
785+
# Euler (or Milstein) scheme
786+
var_t = self.var_step_euler(var_t, dt[i], milstein=milstein)
787+
var_avg += weight[i + 1] * var_t
788+
789+
elif self.scheme == 2:
790+
var_avg = weight[0]*var_t
791+
for i in range(n_dt):
792+
var_t = self.var_step_ncx2(var_t, dt[i])
793+
var_avg += weight[i + 1] * var_t
794+
795+
elif self.scheme == 3:
796+
var_avg = weight[0]*var_t
797+
for i in range(n_dt):
798+
var_t, _ = self.var_step_ncx2_eta(var_t, dt[i])
799+
var_avg += weight[i + 1] * var_t
800+
801+
elif self.scheme == 4:
802+
var_avg = weight[0]*var_t
803+
for i in range(n_dt):
804+
var_t = self.var_step_qe(var_t, dt[i])
805+
var_avg += weight[i + 1] * var_t
806+
807+
elif self.scheme == 5:
808+
m_x, _ = self.x1star_avgvar_mv(dt[0], kk=0)
809+
m_z, _ = self.x2star_avgvar_mv(dt[0], kk=0)
810+
811+
weight *= 2 * m_x
812+
weight_eta = 2 * m_z / n_dt
813+
var_avg = weight[0] * var_t
814+
for i in range(n_dt):
815+
var_t, eta = self.var_step_ncx2_eta(var_t, dt[i])
816+
var_avg += weight[i + 1] * var_t + weight_eta * eta
817+
818+
var_avg += 0.5 * m_z * self.chi_dim()
819+
820+
return var_t, var_avg
821+
822+
def avgvar_var_unexplained(self, texp, dt=None):
823+
"""
824+
Unexplained variance ratio of average variance
825+
This is valid only for time discretisation with Poisson conditioning.
826+
827+
Args:
828+
texp: time to expiry
829+
dt: time step
830+
831+
Returns:
832+
ratio
833+
"""
834+
835+
dt = dt or self.dt
836+
mean, var = self.avgvar_mv(self.sigma, texp)
837+
838+
m_x, v_x = self.x1star_avgvar_mv(dt, kk=0)
839+
m_z, v_z = self.x2star_avgvar_mv(dt, kk=0)
840+
841+
vov2dt = self.vov**2 * dt
842+
unex = (v_x*2 + v_z*4/vov2dt) * mean * dt / texp
843+
return unex / var
844+
845+
800846
class HestonMcTseWan2013(HestonMcGlassermanKim2011):
801847
"""
802848
Almost exact MC for Heston model.
@@ -958,4 +1004,3 @@ def cond_states(self, var_0, texp):
9581004
var_avg = self.draw_x123(var_sum, dt[0], shape_sum) / n_dt
9591005

9601006
return var_t, var_avg
961-

0 commit comments

Comments
 (0)