@@ -25,15 +25,15 @@ def chi_dim(self):
25
25
chi_dim = 4 * self .theta * self .mr / self .vov ** 2
26
26
return chi_dim
27
27
28
- def chi_lambda (self , df ):
28
+ def chi_lambda (self , dt ):
29
29
"""
30
30
Noncentral Chi-square (NCX) distribution's noncentrality parameter
31
31
32
32
Returns:
33
33
noncentrality parameter (scalar)
34
34
"""
35
35
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
37
37
return chi_lambda
38
38
39
39
def phi_exp (self , texp ):
@@ -117,6 +117,14 @@ def cond_states(self, var_0, dt):
117
117
def cond_spot_sigma (self , var_0 , texp ):
118
118
var_final , var_avg = self .cond_states (var_0 , texp )
119
119
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
+
120
128
spot_cond = ((var_final - var_0 ) - self .mr * texp * (self .theta - var_avg )) / self .vov \
121
129
- 0.5 * self .rho * var_avg * texp
122
130
np .exp (self .rho * spot_cond , out = spot_cond )
@@ -126,154 +134,6 @@ def cond_spot_sigma(self, var_0, texp):
126
134
return spot_cond , sigma_cond
127
135
128
136
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
-
277
137
class HestonMcGlassermanKim2011 (HestonMcABC ):
278
138
"""
279
139
Exact simulation using the gamma series based on Glasserman & Kim (2011)
@@ -797,6 +657,192 @@ def cond_states(self, var_0, texp):
797
657
return var_t , var_avg
798
658
799
659
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
+
800
846
class HestonMcTseWan2013 (HestonMcGlassermanKim2011 ):
801
847
"""
802
848
Almost exact MC for Heston model.
@@ -958,4 +1004,3 @@ def cond_states(self, var_0, texp):
958
1004
var_avg = self .draw_x123 (var_sum , dt [0 ], shape_sum ) / n_dt
959
1005
960
1006
return var_t , var_avg
961
-
0 commit comments