@@ -18,7 +18,7 @@ class SabrCondMc(sabr.SabrABC, sv.CondMcBsmABC):
18
18
19
19
def vol_paths (self , tobs , mu = 0 ):
20
20
"""
21
- exp(vov_std B_s - 0.5*vov_std ^2 * s) where s = 0, ..., 1, vov_std = vov* sqrt(T)
21
+ exp(vovn B_s - 0.5*vovn ^2 * s) where s = 0, ..., 1, vovn = vov * sqrt(T)
22
22
23
23
Args:
24
24
tobs: observation time (array)
@@ -29,12 +29,12 @@ def vol_paths(self, tobs, mu=0):
29
29
30
30
texp = tobs [- 1 ]
31
31
tobs01 = tobs / texp # normalized time: 0<s<1
32
- vov_std = self .vov * np .sqrt (texp )
32
+ vovn = self .vov * np .sqrt (texp )
33
33
34
34
log_sig_s = self ._bm_incr (tobs01 , cum = True ) # B_s (0 <= s <= 1)
35
35
log_rn_deriv = 0.0 if mu == 0 else - mu * (log_sig_s [- 1 , :] + 0.5 * mu )
36
36
37
- log_sig_s = vov_std * (log_sig_s + (mu - 0.5 * vov_std ) * tobs01 [:, None ])
37
+ log_sig_s = vovn * (log_sig_s + (mu - 0.5 * vovn ) * tobs01 [:, None ])
38
38
log_sig_s = np .insert (log_sig_s , 0 , np .zeros (log_sig_s .shape [1 ]), axis = 0 )
39
39
return np .exp (log_sig_s ), log_rn_deriv
40
40
@@ -87,10 +87,10 @@ def mass_zero(self, spot, texp, log=False, mu=0):
87
87
* np .power (spot , 1.0 - self .beta )
88
88
/ (self .sigma * (1.0 - self .beta ))
89
89
)
90
- vov_std = self .vov * np .sqrt (texp )
90
+ vovn = self .vov * np .sqrt (texp )
91
91
92
92
if mu is None :
93
- mu = 0.5 * (vov_std + np .log (1 + eta ** 2 ) / vov_std )
93
+ mu = 0.5 * (vovn + np .log (1 + eta ** 2 ) / vovn )
94
94
# print(f'mu = {mu}')
95
95
96
96
fwd_cond , vol_cond , log_rn_deriv = self .cond_spot_sigma (texp , mu = mu )
@@ -117,32 +117,35 @@ class SabrMcExactCai2017(sabr.SabrABC, sv.CondMcBsmABC):
117
117
- Cai N, Song Y, Chen N (2017) Exact Simulation of the SABR Model. Oper Res 65:931–951. https://doi.org/10.1287/opre.2017.1617
118
118
"""
119
119
m_inv = 20
120
- n_inv = 35
120
+ m_euler = 20
121
+ n_euler = 35
121
122
comb_coef = None
122
123
nn = None
123
124
124
- def set_mc_params (self , n_path = 10000 , m_inv = 20 , n_inv = 35 , rn_seed = None , antithetic = True ):
125
+ def set_mc_params (self , n_path = 10000 , m_inv = 20 , m_euler = 20 , n_euler = 35 , rn_seed = None , antithetic = True ):
125
126
"""
126
127
Set MC parameters
127
128
128
129
Args:
129
130
n_path: number of paths
130
- m_inv: parameter M used in Laplace inversion
131
- n_inv: parameter n used in Laplace inversion
131
+ m_inv: parameter M in Laplace inversion, Eq. (16)
132
+ m_euler: parameter m in Euler transformation E(m,n)
133
+ n_euler: parameter n in Euler transformation E(m,n)
132
134
rn_seed: random number seed
133
135
antithetic: antithetic
134
136
"""
135
137
self .n_path = int (n_path )
136
138
self .m_inv = m_inv
137
- self .n_inv = n_inv
139
+ self .m_euler = m_euler
140
+ self .n_euler = n_euler
138
141
self .dt = None
139
142
self .rn_seed = rn_seed
140
143
self .antithetic = antithetic
141
144
self .rn_seed = rn_seed
142
145
self .rng = np .random .default_rng (rn_seed )
143
- self .comb_coef = spsp .comb (self .m_inv , np .arange (0 , self .m_inv + 0.1 )) * np .power (0.5 , self .m_inv )
146
+ self .comb_coef = spsp .comb (self .m_euler , np .arange (0 , self .m_euler + 0.1 )) * np .power (0.5 , self .m_euler )
144
147
assert abs (self .comb_coef .sum ()- 1 ) < 1e-8
145
- self .nn = np .arange (0 , self .m_inv + self .n_inv + 0.1 )
148
+ self .nn = np .arange (0 , self .m_euler + self .n_euler + 0.1 )
146
149
147
150
def vol_paths (self , tobs , mu = 0 ):
148
151
return None
@@ -158,8 +161,12 @@ def sigma_final(self, vovn):
158
161
-------
159
162
vol at maturity
160
163
"""
161
- zz = self .rng .normal (size = self .n_path // 2 )
162
- zz = np .hstack ([zz , - zz ])
164
+
165
+ if self .antithetic :
166
+ zz = self .rng .normal (size = self .n_path // 2 )
167
+ zz = np .hstack ([zz , - zz ])
168
+ else :
169
+ zz = self .rng .normal (size = self .n_path )
163
170
164
171
sigma_T = np .exp (vovn * (zz - vovn / 2 ))
165
172
return sigma_T
@@ -186,67 +193,67 @@ def cond_laplace(self, theta, vovn, sigma_T):
186
193
return np .exp ((x ** 2 - phi ** 2 ) / (2 * vovn ** 2 )) / theta
187
194
188
195
def inv_laplace (self , u , vovn , sigma_T ):
189
- '''
196
+ """
190
197
Eq. (16) in the article
191
198
Return the original function from transform function
192
- Parameters
193
- ----------
194
- u: variable
195
- vovn: vov * sqrt(texp)
196
- sigma_T: simulated sigma at time of expiration
197
- Returns
198
- -------
199
- original functions with parameter u
200
- '''
199
+
200
+ Args:
201
+ u: original variable
202
+ vovn: vov * sqrt(texp)
203
+ sigma_T: final volatility
204
+
205
+ Returns:
206
+ original function value at u
207
+ """
201
208
202
209
## index from 0 to m + n
203
210
ss_j = self .cond_laplace ((self .m_inv - 2j * np .pi * self .nn ) / (2 * u ), vovn , sigma_T ).real
204
211
term1 = 0.5 * ss_j [0 ]
205
212
ss_j [1 ::2 ] *= - 1
206
213
np .cumsum (ss_j , out = ss_j )
207
- term2 = np .sum (self .comb_coef * ss_j [self .n_inv :])
214
+ term2 = np .sum (self .comb_coef * ss_j [self .n_euler :])
208
215
209
216
origin_L = np .exp (self .m_inv / 2 ) / u * (- term1 + term2 )
210
217
211
218
return origin_L
212
219
213
- def conditional_state (self , vovn ):
220
+ def cond_int_var (self , vovn , sigma_final ):
214
221
"""
215
- Return Exact integrated variance from samples of disturibution
216
- The returns values are for sigma_0 = self.sigma and t = T
217
- Parameters
218
- ----------
219
- vovn: float
220
- time-to-expiry
221
- Returns
222
- -------
223
- (sigma at the time of expiration, Exact integrated variance)
222
+ Normalized integraged variance samples.
223
+
224
+ Args:
225
+ vovn: vov * sqrt(texp)
226
+ sigma_final: final volatility
227
+
228
+ Returns:
229
+ (n_path, 1) array
224
230
"""
225
231
226
- u_rn = self .rng .uniform (size = self .n_path // 2 )
227
- u_rn = np .hstack ([u_rn , 1 - u_rn ])
232
+ if self .antithetic :
233
+ u_rn = self .rng .uniform (size = self .n_path // 2 )
234
+ u_rn = np .hstack ([u_rn , 1 - u_rn ])
235
+ else :
236
+ u_rn = self .rng .uniform (size = self .n_path )
228
237
229
238
int_var = np .zeros (self .n_path )
230
- sigma_final = self .sigma_final (vovn )
231
239
232
240
for i in range (self .n_path ):
233
241
obj_func = lambda x : self .inv_laplace (x , vovn , sigma_final [i ]) - u_rn [i ]
234
242
235
- if sigma_final [i ] > 1 :
236
- x0 = 1 / sigma_final [i ]
237
- else :
238
- x0 = 2 / (1 + sigma_final [i ])
239
-
240
243
sol = spop .brentq (obj_func , 0.000001 , 100 )
241
244
int_var [i ] = 1 / sol
242
- return sigma_final , int_var
245
+
246
+ return int_var
243
247
244
248
def cond_spot_sigma (self , texp ):
245
249
246
250
rhoc = np .sqrt (1.0 - self .rho ** 2 )
247
251
rho_sigma = self .rho * self .sigma
248
252
vovn = self .vov * np .sqrt (texp )
249
- sigma_final , int_var = self .conditional_state (vovn )
253
+
254
+ sigma_final = self .sigma_final (vovn )
255
+ int_var = self .cond_int_var (vovn , sigma_final )
256
+ #print(1/np.max(int_var), 1/np.min(int_var))
250
257
251
258
vol_cond = rhoc * np .sqrt (int_var )
252
259
if np .isclose (self .beta , 0 ):
@@ -257,6 +264,25 @@ def cond_spot_sigma(self, texp):
257
264
)
258
265
return fwd_cond , vol_cond
259
266
267
+ def price (self , strike , spot , texp , cp = 1 ):
268
+ # The formula is exactly same as that of SabrCondMc except rn_deriv. Need to merge
269
+ fwd = self .forward (spot , texp )
270
+ fwd_cond , vol_cond = self .cond_spot_sigma (texp )
271
+ if np .isclose (self .beta , 0 ):
272
+ base_model = self ._m_base (self .sigma * vol_cond , is_fwd = True )
273
+ price_grid = base_model .price (strike [:, None ], fwd + fwd_cond , texp , cp = cp )
274
+ price = np .mean (price_grid , axis = 1 )
275
+ else :
276
+ alpha = self .sigma / np .power (spot , 1.0 - self .beta )
277
+ kk = strike / fwd
278
+
279
+ base_model = self ._m_base (alpha * vol_cond , is_fwd = True )
280
+ price_grid = base_model .price (kk [:, None ], fwd_cond , texp , cp = cp )
281
+ price = fwd * np .mean (price_grid , axis = 1 )
282
+
283
+ return price
284
+
285
+
260
286
# The algorithem below is about pricing when 0<=beta<1
261
287
def simu_ST (self , beta , VT , spot ):
262
288
'''
@@ -404,48 +430,3 @@ def theta_func(mu2, yita, s):
404
430
+ 2 * theta_s (s ) / sigma ) ** 0.5
405
431
cdf = spst .norm .cdf (z )
406
432
return cdf
407
-
408
- def price (self , strike , spot , texp , cp = 1 ):
409
- """
410
- over ride the price method in parent class to incorporate 0< beta < 1
411
- if beta = 1 or 0, use bsm model
412
- if 0 < beta < 1, use mc simulation
413
- Args:
414
- strike: 1d array, an array of strike prices
415
- spot: float, spot prices
416
- texp: time to expiry
417
- cp:
418
- Returns:
419
- 1d array of option prices
420
- when x < 500 and l < 500:
421
- equation (19) in Cai(2017)
422
- The recursive alogorithm propose by Ding(1992) to calculate chi-2 cdf
423
- when x > 500 or l > 500:
424
- analytic approximation of Penev and Raykov(2000)
425
- Example(beta = 1):
426
- fwd = 100
427
- strike = np.arange(50,151,10)
428
- texp = 1
429
- params = {"sigma": 0.2, "vov": 0.3, "rho": 0, "beta":1}
430
- sabr_mc_model = pyfe.ExactMcSabr(**params)
431
- sabr_mc_model.set_mc_params(n_path=1000)
432
- price = sabr_mc_model.price(strike, fwd, texp)
433
- """
434
-
435
- if isinstance (strike , float ):
436
- strike = np .array ([strike ])
437
- if self .beta == 1 :
438
- fwd_cond , vol_cond = self .cond_spot_sigma (texp )
439
- base_model = self .base_model (self .sigma * vol_cond [:, None ])
440
- price_grid = base_model .price (strike , spot * fwd_cond [:, None ], texp , cp )
441
- price = np .mean (price_grid , axis = 0 )
442
- return price [0 ] if price .size == 1 else price
443
- else : # 0 < beta < 1
444
- assert self .rho == 0 , "rho has to be 0 fro 0<beta<1"
445
- price_lst = []
446
- for kk in strike :
447
- _ , VT = self .conditional_state (texp )
448
- price = np .maximum (self .simu_ST (self .beta , VT , spot ) - kk , 0 ) * np .exp (- self .intr * texp )
449
- price = np .mean (price )
450
- price_lst .append (price )
451
- return np .array (price_lst )
0 commit comments