@@ -309,7 +309,7 @@ def price(self, strike, spot, texp, cp=1):
309
309
return df * price
310
310
311
311
312
- class NsvhQuadInt (sabr .SabrABC ):
312
+ class NsvhGaussQuad (sabr .SabrABC ):
313
313
"""
314
314
Quadrature integration method of Hyperbolic Normal Stochastic Volatility (NSVh) model.
315
315
@@ -325,22 +325,22 @@ class NsvhQuadInt(sabr.SabrABC):
325
325
>>> import numpy as np
326
326
>>> import pyfeng as pf
327
327
>>> #### Nsvh1: comparison with analytic pricing
328
- >>> m1 = pf.NsvhQuadInt (sigma=20, vov=0.8, rho=-0.3, lam=1.0)
328
+ >>> m1 = pf.NsvhGaussQuad (sigma=20, vov=0.8, rho=-0.3, lam=1.0)
329
329
>>> m2 = pf.Nsvh1(sigma=20, vov=0.8, rho=-0.3)
330
330
>>> p1 = m1.price(np.arange(80, 121, 10), 100, 1.2)
331
331
>>> p2 = m2.price(np.arange(80, 121, 10), 100, 1.2)
332
332
>>> p1 - p2
333
333
array([0.00345526, 0.00630649, 0.00966333, 0.00571175, 0.00017924])
334
334
>>> #### Normal SABR: comparison with vol approximation
335
- >>> m1 = pf.NsvhQuadInt (sigma=20, vov=0.8, rho=-0.3, lam=0.0)
335
+ >>> m1 = pf.NsvhGaussQuad (sigma=20, vov=0.8, rho=-0.3, lam=0.0)
336
336
>>> m2 = pf.SabrNormVolApprox(sigma=20, vov=0.8, rho=-0.3)
337
337
>>> p1 = m1.price(np.arange(80, 121, 10), 100, 1.2)
338
338
>>> p2 = m2.price(np.arange(80, 121, 10), 100, 1.2)
339
339
>>> p1 - p2
340
340
array([-0.17262802, -0.10160687, -0.00802731, 0.0338126 , 0.01598512])
341
341
342
342
References:
343
- Choi J (2023), Unpublished Working Paper.
343
+ Choi J (2023), Option pricing under the normal SABR model with Gaussian quadratures. Unpublished Working Paper.
344
344
345
345
"""
346
346
@@ -372,16 +372,13 @@ def price(self, strike, spot, texp, cp=1):
372
372
### axis 1: nodes of x,y,z , get the weight of z,v
373
373
z_value , z_weight = spsp .roots_hermitenorm (self .n_quad [0 ])
374
374
z_weight /= np .sqrt (2 * np .pi )
375
+ z_weight /= np .sum (np .exp (vovn / 2 * z_value )* z_weight ) / np .exp (vovn ** 2 / 8 )
376
+ #print(np.sum(np.exp(-vovn/2*z_value)*z_weight) - np.exp(vovn**2/8))
375
377
376
- if self .n_quad [1 ] is not None :
377
- # quadrature point & weight for exp(-v) derived from sqrt(v) * np.exp(-v/2)
378
- v_value , v_weight = spsp .roots_genlaguerre (self .n_quad [1 ], 0.5 )
379
- v_weight *= 2 / np .sqrt (v_value )
380
- v_value *= 2.0
381
- else :
382
- # uniform grid from v=0 to 40 from np.exp(-20) ~ 2e-9
383
- v_value = np .arange (1 , 8001 ) / 200
384
- v_weight = np .full_like (v_value , 1 / 200 ) * np .exp (- v_value / 2 )
378
+ # quadrature point & weight for exp(-v/2)/(2 pi) derived from sqrt(v) * np.exp(-v)
379
+ v_value , v_weight = spsp .roots_genlaguerre (self .n_quad [1 ], 0.5 )
380
+ v_weight /= np .pi * np .sqrt (v_value )
381
+ v_value *= 2.0
385
382
386
383
### axis 0: dependence on v
387
384
v_value = v_value [:, None ]
@@ -408,7 +405,8 @@ def price(self, strike, spot, texp, cp=1):
408
405
theta_mat = np .arccos (np .abs (g_vec ) / h_mat )
409
406
410
407
int_z_v = np .sqrt (h_mat ** 2 - g_vec ** 2 ) - np .abs (g_vec ) * theta_mat
411
- int_z = np .sum (int_z_v * v_weight , axis = 0 ) / (2 * np .pi ) # integrating over v (column)
408
+
409
+ int_z = np .sum (int_z_v * v_weight , axis = 0 ) # integrating over v (column)
412
410
int_z [:] = int_z * np .exp (- v_0 / 2 ) + np .fmax (cp [i ] * g_vec , 0.0 ) # in-place operation
413
411
414
412
price [i ] = np .sum (int_z * z_weight )
@@ -419,3 +417,52 @@ def price(self, strike, spot, texp, cp=1):
419
417
price = price [0 ]
420
418
421
419
return price
420
+
421
+ def cdf (self , strike , spot , texp , cp = - 1 ):
422
+ fwd = self .forward (spot , texp )
423
+ _ , _ , rhoc , rho2 , vovn = self ._variables (1.0 , texp )
424
+
425
+ ### axis 1: nodes of x,y,z , get the weight of z,v
426
+ z_value , z_weight = spsp .roots_hermitenorm (self .n_quad [0 ])
427
+ z_weight /= np .sqrt (2 * np .pi )
428
+ z_weight /= np .sum (np .exp (vovn / 2 * z_value )* z_weight ) / np .exp (vovn ** 2 / 8 )
429
+
430
+ # quadrature point & weight for exp(-v/2)/(2 pi) derived from np.exp(-v)
431
+ v_value , v_weight = spsp .roots_genlaguerre (self .n_quad [1 ], 0.0 )
432
+ v_weight /= np .pi
433
+ v_value *= 2
434
+
435
+ ### axis 0: dependence on v
436
+ v_value = v_value [:, None ]
437
+ v_weight = v_weight [:, None ]
438
+
439
+ vov_var = np .exp (0.5 * self .lam * vovn ** 2 )
440
+
441
+ #### effective strike
442
+ strike_eff = (self .vov / self .sigma ) * (strike - fwd )
443
+ scalar_output = np .isscalar (strike_eff )
444
+
445
+ strike_eff , cp = np .broadcast_arrays (np .atleast_1d (strike_eff ), cp )
446
+
447
+ u_hat = (z_value + 0.5 * self .lam * vovn ) # column (z direction)
448
+ exp_plus = np .exp (vovn * u_hat / 2 )
449
+ z_star_cosh = (exp_plus ** 2 + 1 / exp_plus ** 2 ) / 2
450
+ cdf = np .zeros_like (strike , dtype = float )
451
+
452
+ for i , k_eff in enumerate (strike_eff ):
453
+ g_vec = self .rho * exp_plus - (self .rho * vov_var + k_eff ) / exp_plus
454
+ temp1 = z_star_cosh + 0.5 * g_vec ** 2 / (1 - rho2 )
455
+ v_0 = (np .arccosh (temp1 ) / vovn ) ** 2 - u_hat ** 2
456
+ h_mat = rhoc * np .sqrt (
457
+ 2 * np .cosh (vovn * np .sqrt ((u_hat ** 2 + v_0 + v_value ))) - 2 * np .cosh (vovn * u_hat ))
458
+ theta_mat = np .arccos (np .abs (g_vec ) / h_mat )
459
+
460
+ int_z = np .sum (theta_mat * np .exp (- v_0 / 2 ) * v_weight , axis = 0 ) # integrating over v (column)
461
+ int_z [cp [i ]* g_vec > 0 ] = 1.0 - int_z [cp [i ]* g_vec > 0 ]
462
+ int_z *= np .exp (- z_value * vovn / 2 - vovn ** 2 / 8 )
463
+ cdf [i ] = np .sum (int_z * z_weight )
464
+
465
+ if scalar_output :
466
+ cdf = cdf [0 ]
467
+
468
+ return cdf
0 commit comments