@@ -68,52 +68,52 @@ def d1sigma(d1, logk):
68
68
return sig
69
69
70
70
@staticmethod
71
- def vega_std (sigma , logk ):
71
+ def price_std (sigma , logk , sign = 1 , type = 1 ):
72
72
"""
73
- Standardized Vega
73
+ Price (ratio) for standardized parameters
74
74
75
- Args:
76
- sigma: volatility
77
- logk: log strike
75
+ type=0: Price
76
+ Option Price = N(d1) - k N(d2) = n(d1) * [R(-d1) - R(-d2)]
77
+ where R(x) = N(-x)/n(x) is Mills ratio
78
78
79
- Returns:
79
+ type=1: Price-to-vega ratio
80
+ Option Price / Vega = [N(d1) - k N(d2)]/n(d1) = R(-d1) - R(-d2) for sign = 1
81
+ (1 - Option Price) / Vega = [1 - N(d1) + k N(d2)]/n(d1) = R(d1) + R(-d2) for sign = -1
80
82
81
- """
82
- # don't directly compute d1 just in case sigma_std is infty
83
- # handle the case logk = sigma = 0 (ATM)
84
- d1 = np .where (logk == 0. , 0. , - logk / sigma )
85
- d1 += 0.5 * sigma
86
- vega = spst .norm ._pdf (d1 )
87
- return vega
83
+ type=-1: Vega = n(d1)
88
84
89
- @staticmethod
90
- def price_vega_std (sigma , logk , sign = 1 , price = False ):
91
- """
92
- Price-to-vega pv.
93
- Option Price / Vega = (N(d1) - k N(d2))/n(d1) = R(-d1) - R(-d2) for sign = 1
94
- (1 - Option Price) / Vega = (1 - N(d1) + k N(d2))/n(d1) = R(d1) + R(-d2) for sign = -1
95
- where R(x) = N(-x)/n(x) is Mills pv
85
+ type=2: Price-to-delta ratio
86
+ Option Price / Delta = [N(d1) - k N(d2)]/N(d1) = 1 - R(-d2)/R(-d1)
87
+
88
+ These values are used in calculating upper/lower bound of IV in Choi et al. (2025)
96
89
97
90
Args:
98
91
sigma: volatility
99
92
logk: log strike
100
93
sign: -1 for complementary price. +1 by default
101
- price: multiply vega so return price if True. False by default
94
+ type: 0 for price, 1 for price-to-vega (default), -1 for vega, 2 for price-to-delta
102
95
103
96
References:
104
- Choi J, Huh J, Su N (2023 ) Tighter “Uniform Bounds for Black-Scholes Implied Volatility” and the applications to root-finding
97
+ Choi J, Huh J, and Su N (2025 ) Tighter “Uniform Bounds for Black-Scholes Implied Volatility” and the applications to root-finding
105
98
106
99
Returns:
107
-
100
+ scaled price (ratio) value
108
101
"""
109
102
# don't directly compute d1 just in case sigma_std is infty
110
103
# handle the case logk = sigma = 0 (ATM)
111
- d0 = np .array (np .where (logk == 0. , 0. , - logk / sigma ))
112
- sigma = np .broadcast_to (sigma , d0 .shape )
113
- pv = np .array (MathFuncs .mills_ratio (- sign * (d0 + sigma / 2. )) - sign * MathFuncs .mills_ratio (- d0 + sigma / 2. ))
104
+ sh = np .broadcast_shapes (np .shape (logk ), np .shape (sigma ))
105
+ sigma = np .broadcast_to (sigma , shape = sh )
106
+ d0 = np .full (sh , fill_value = - logk )
107
+ np .divide (d0 , sigma , out = d0 , where = (d0 != 0.0 ))
108
+
109
+ if type == - 1 :
110
+ return spst .norm ._pdf (d0 + sigma / 2. )
111
+
112
+ R_left = MathFuncs .mills_ratio (- sign * (d0 + 0.5 * sigma ))
113
+ rv = R_left - sign * MathFuncs .mills_ratio (- d0 + 0.5 * sigma )
114
114
115
115
## Correct pv values for very small sigma/d0
116
- idx = (sigma / np .fmax (1.0 , - d0 ) < 1e-3 ) & (sign > 0 )
116
+ idx = (sigma > 0.0 ) & ( sigma / np .fmax (1.0 , - d0 ) < 1e-3 ) & (sign > 0 )
117
117
if np .any (idx ):
118
118
sig_idx = sigma [idx ]
119
119
d0_idx = d0 [idx ]
@@ -129,35 +129,14 @@ def price_vega_std(sigma, logk, sign=1, price=False):
129
129
130
130
R_d3 = (d0_idx ** 2 + 3. )* R_d1 - 1.
131
131
# next term is sig_idx**4/1920 * R_d5
132
- pv [idx ] = (R_d1 + sig_idx ** 2 / 24. * R_d3 )* sig_idx
133
-
134
- if price :
135
- pv *= spst .norm ._pdf (d0 + sigma / 2. )
132
+ rv [idx ] = (R_d1 + sig_idx ** 2 / 24. * R_d3 )* sig_idx
136
133
137
- return pv
138
-
139
- @staticmethod
140
- def price_delta_std (sigma , logk ):
141
- """
142
- Price-to-delta ratio, 1 - R(-d2)/R(-d1) for Mills ratio R(x) = N(-x)/n(x).
143
- It's used in calculating upper/lower bound of IV in Choi et al. (2023)
144
-
145
- Args:
146
- sigma: volatility
147
- logk: log strike
148
-
149
- Returns:
150
-
151
- References:
152
- Choi J, Huh J, Su N (2023) Tighter “Uniform Bounds for Black-Scholes Implied Volatility” and the applications to root-finding
153
- """
154
-
155
- # don't directly compute d1 just in case sigma_std is infty
156
- # handle the case logk = sigma = 0 (ATM)
157
- d0 = np .where (logk == 0. , 0. , - logk / sigma )
158
- ratio = 1.0 - MathFuncs .mills_ratio (- d0 + sigma / 2. ) / MathFuncs .mills_ratio (- d0 - sigma / 2. )
134
+ if type == 0 : # price
135
+ rv *= spst .norm ._pdf (d0 + sigma / 2. )
136
+ elif type == 2 : # price-to-delta
137
+ rv /= R_left
159
138
160
- return ratio
139
+ return rv
161
140
162
141
def vega (self , strike , spot , texp , cp = 1 ):
163
142
@@ -329,7 +308,7 @@ def impvol_naive(self, price, strike, spot, texp, cp=1, setval=False, n_iter=32)
329
308
330
309
# Exclude optoin price below intrinsic value or above max value (1 for call or k for put)
331
310
# ind_solve can be scalar or array. scalar can be fine in np.abs(p_err[ind_solve])
332
- ind_solve = (price_std - p_min > Bsm .IMPVOL_TOL ) & (p_max - price_std > Bsm .IMPVOL_TOL )
311
+ ind_solve = (price_std - p_min > self .IMPVOL_TOL ) & (p_max - price_std > self .IMPVOL_TOL )
333
312
334
313
# initial guess = inflection point in sigma (volga=0)
335
314
_sigma = np .ones_like (ind_solve )* np .sqrt (2 * np .abs (np .log (strike_std ))/ texp )
@@ -348,11 +327,11 @@ def impvol_naive(self, price, strike, spot, texp, cp=1, setval=False, n_iter=32)
348
327
# print(k, p_err_max, _sigma)
349
328
350
329
# ignore the error of the elements with ind_solve = False
351
- if p_err_max < Bsm .IMPVOL_TOL :
330
+ if p_err_max < self .IMPVOL_TOL :
352
331
break
353
332
#print(k)
354
333
355
- if p_err_max >= Bsm .IMPVOL_TOL :
334
+ if p_err_max >= self .IMPVOL_TOL :
356
335
warn_msg = f"impvol_newton did not converged within { k } iterations: max error = { p_err_max } "
357
336
warnings .warn (warn_msg , Warning )
358
337
@@ -361,14 +340,14 @@ def impvol_naive(self, price, strike, spot, texp, cp=1, setval=False, n_iter=32)
361
340
362
341
# Though not error is above tolerance, if the price is close to min or max, set 0 or inf
363
342
_sigma = np .where (
364
- (np .abs (p_err ) >= Bsm .IMPVOL_TOL )
365
- & (np .abs (price_std - p_min ) <= Bsm .IMPVOL_TOL ),
343
+ (np .abs (p_err ) >= self .IMPVOL_TOL )
344
+ & (np .abs (price_std - p_min ) <= self .IMPVOL_TOL ),
366
345
0 ,
367
346
_sigma ,
368
347
)
369
348
_sigma = np .where (
370
- (np .abs (p_err ) >= Bsm .IMPVOL_TOL )
371
- & (np .abs (price_std - p_max ) <= Bsm .IMPVOL_TOL ),
349
+ (np .abs (p_err ) >= self .IMPVOL_TOL )
350
+ & (np .abs (price_std - p_max ) <= self .IMPVOL_TOL ),
372
351
np .inf ,
373
352
_sigma ,
374
353
)
@@ -395,7 +374,7 @@ def impvol_log(self, price, strike, spot, texp, cp=1, setval=False, n_iter=8):
395
374
setval: if True, sigma is set with the solved implied volatility
396
375
397
376
References:
398
- Choi J, Huh J, Su N (2023 ) Tighter “Uniform Bounds for Black-Scholes Implied Volatility” and the applications to root-finding
377
+ Choi J, Huh J, and Su N (2025 ) Tighter “Uniform Bounds for Black-Scholes Implied Volatility” and the applications to root-finding
399
378
400
379
Returns:
401
380
implied volatility
@@ -415,22 +394,22 @@ def impvol_log(self, price, strike, spot, texp, cp=1, setval=False, n_iter=8):
415
394
sigma = np .where (
416
395
(logk == 0. ) & (p < 1e-4 ),
417
396
MathConsts .M_SQRT2PI * p * (1.0 + np .pi * p ** 2 / 12. ),
418
- self .d1sigma (spst .norm .ppf (p * (kk + p )/ (2 * p + (kk - 1. ))), logk ) # Lower bound from the paper
397
+ self .d1sigma (spst .norm ._ppf (p * (kk + p )/ (2 * p + (kk - 1. ))), logk ) # Lower bound from the paper
419
398
)
420
399
421
400
log_p_2pi = np .log (p ) + MathConsts .M_LN2PI_2 # 0.5*np.log(2*np.pi)
422
401
423
402
for k in range (n_iter ):
424
- p2v = self .price_vega_std (sigma , logk ) # pf.Bsm.price_vega_ratio
403
+ pv = self .price_std (sigma , logk , type = 1 ) # price-to-vega ratio
425
404
d1 = np .where (logk == 0. , 0. , - logk / sigma ) + 0.5 * sigma
426
- p_log_err = - 0.5 * d1 ** 2 + np .log (p2v ) - log_p_2pi
405
+ p_log_err = - 0.5 * d1 ** 2 + np .log (pv ) - log_p_2pi
427
406
p_log_err_max = np .amax (np .abs (p_log_err ))
428
- if p_log_err_max < Bsm .IMPVOL_TOL :
407
+ if p_log_err_max < self .IMPVOL_TOL :
429
408
break
430
- sigma -= p_log_err * p2v
409
+ sigma -= p_log_err * pv
431
410
432
- if p_log_err_max >= Bsm .IMPVOL_TOL :
433
- warn_msg = f"impvol_newton did not converged within { k } iterations: max error = { p_log_err_max } "
411
+ if p_log_err_max >= self .IMPVOL_TOL :
412
+ warn_msg = f"impvol_newton did not converged within { k } iterations: max log error = { p_log_err_max } "
434
413
warnings .warn (warn_msg , Warning )
435
414
436
415
sigma /= np .sqrt (texp )
0 commit comments