Skip to content

Commit 5d5cfd3

Browse files
authored
Merge pull request #181 from PyFE/bsiv
Merged standard prices to price_std
2 parents ae9efd4 + bd74aec commit 5d5cfd3

File tree

1 file changed

+48
-69
lines changed

1 file changed

+48
-69
lines changed

pyfeng/bsm.py

Lines changed: 48 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -68,52 +68,52 @@ def d1sigma(d1, logk):
6868
return sig
6969

7070
@staticmethod
71-
def vega_std(sigma, logk):
71+
def price_std(sigma, logk, sign=1, type=1):
7272
"""
73-
Standardized Vega
73+
Price (ratio) for standardized parameters
7474
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
7878
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
8082
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)
8884
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)
9689
9790
Args:
9891
sigma: volatility
9992
logk: log strike
10093
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
10295
10396
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
10598
10699
Returns:
107-
100+
scaled price (ratio) value
108101
"""
109102
# don't directly compute d1 just in case sigma_std is infty
110103
# 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)
114114

115115
## 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)
117117
if np.any(idx):
118118
sig_idx = sigma[idx]
119119
d0_idx = d0[idx]
@@ -129,35 +129,14 @@ def price_vega_std(sigma, logk, sign=1, price=False):
129129

130130
R_d3 = (d0_idx**2 + 3.)*R_d1 - 1.
131131
# 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
136133

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
159138

160-
return ratio
139+
return rv
161140

162141
def vega(self, strike, spot, texp, cp=1):
163142

@@ -329,7 +308,7 @@ def impvol_naive(self, price, strike, spot, texp, cp=1, setval=False, n_iter=32)
329308

330309
# Exclude optoin price below intrinsic value or above max value (1 for call or k for put)
331310
# 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)
333312

334313
# initial guess = inflection point in sigma (volga=0)
335314
_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)
348327
# print(k, p_err_max, _sigma)
349328

350329
# 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:
352331
break
353332
#print(k)
354333

355-
if p_err_max >= Bsm.IMPVOL_TOL:
334+
if p_err_max >= self.IMPVOL_TOL:
356335
warn_msg = f"impvol_newton did not converged within {k} iterations: max error = {p_err_max}"
357336
warnings.warn(warn_msg, Warning)
358337

@@ -361,14 +340,14 @@ def impvol_naive(self, price, strike, spot, texp, cp=1, setval=False, n_iter=32)
361340

362341
# Though not error is above tolerance, if the price is close to min or max, set 0 or inf
363342
_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),
366345
0,
367346
_sigma,
368347
)
369348
_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),
372351
np.inf,
373352
_sigma,
374353
)
@@ -395,7 +374,7 @@ def impvol_log(self, price, strike, spot, texp, cp=1, setval=False, n_iter=8):
395374
setval: if True, sigma is set with the solved implied volatility
396375
397376
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
399378
400379
Returns:
401380
implied volatility
@@ -415,22 +394,22 @@ def impvol_log(self, price, strike, spot, texp, cp=1, setval=False, n_iter=8):
415394
sigma = np.where(
416395
(logk == 0.) & (p < 1e-4),
417396
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
419398
)
420399

421400
log_p_2pi = np.log(p) + MathConsts.M_LN2PI_2 # 0.5*np.log(2*np.pi)
422401

423402
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
425404
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
427406
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:
429408
break
430-
sigma -= p_log_err * p2v
409+
sigma -= p_log_err * pv
431410

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}"
434413
warnings.warn(warn_msg, Warning)
435414

436415
sigma /= np.sqrt(texp)

0 commit comments

Comments
 (0)