11import numpy as np
22import scipy .stats as spst
3+ import mathpf
34import warnings
45
56from .opt_abc import OptABC , OptAnalyticABC
67from .params import BsmParams
7- from .util import MathFuncs , MathConsts
8+ from .util import MathConsts
89from .disthelper import DistLognormal
910
1011class Bsm (BsmParams , OptAnalyticABC ):
@@ -66,7 +67,7 @@ def d1sigma(d1, logk):
6667 return sig
6768
6869 @staticmethod
69- def price_std (sigma , logk , sign = 1 , type = 1 ):
70+ def price_std (sigma , logk , theta = 1 , type = 1 ):
7071 """
7172 Price (ratio) for standardized parameters
7273
@@ -75,8 +76,8 @@ def price_std(sigma, logk, sign=1, type=1):
7576 where R(x) = N(-x)/n(x) is Mills ratio
7677
7778 type=1: Price-to-vega ratio
78- Option Price / Vega = [N(d1) - k N(d2)]/n(d1) = R(-d1) - R(-d2) for sign = 1
79- (1 - Option Price) / Vega = [1 - N(d1) + k N(d2)]/n(d1) = R(d1) + R(-d2) for sign = -1
79+ Option Price / Vega = [N(d1) - k N(d2)]/n(d1) = R(-d1) - R(-d2) for theta = 1
80+ (1 - Option Price) / Vega = [1 - N(d1) + k N(d2)]/n(d1) = R(d1) + R(-d2) for theta = -1
8081
8182 type=-1: Vega = n(d1)
8283
@@ -88,7 +89,7 @@ def price_std(sigma, logk, sign=1, type=1):
8889 Args:
8990 sigma: volatility
9091 logk: log strike
91- sign : -1 for complementary price. +1 by default
92+ theta : -1 for complementary price. +1 by default
9293 type: 0 for price, 1 for price-to-vega (default), -1 for vega, 2 for price-to-delta
9394
9495 References:
@@ -97,40 +98,40 @@ def price_std(sigma, logk, sign=1, type=1):
9798 Returns:
9899 scaled price (ratio) value
99100 """
100- # don't directly compute d1 just in case sigma_std is infty
101- # handle the case logk = sigma = 0 (ATM)
101+ # m0 = logk/sigma (as in bsiv.py); computed carefully since sigma may be 0/inf
102+ # and logk = sigma = 0 is the ATM case. d1 = -m0 + sigma/2, d2 = -m0 - sigma/2.
102103 sh = np .broadcast_shapes (np .shape (logk ), np .shape (sigma ))
103104 sigma = np .broadcast_to (sigma , shape = sh )
104- d0 = np .full (sh , fill_value = - logk )
105- np .divide (d0 , sigma , out = d0 , where = (d0 != 0.0 ))
105+ m0 = np .full (sh , fill_value = logk , dtype = float )
106+ np .divide (m0 , sigma , out = m0 , where = (m0 != 0.0 ))
106107
107108 if type == - 1 :
108- return spst .norm ._pdf (d0 + sigma / 2. )
109+ return spst .norm ._pdf (- m0 + sigma / 2. )
109110
110- R_left = MathFuncs . mills_ratio ( - sign * (d0 + 0.5 * sigma ))
111- rv = R_left - sign * MathFuncs . mills_ratio ( - d0 + 0.5 * sigma )
111+ R_left = mathpf . millsratio ( theta * (m0 - 0.5 * sigma )) # R(-d1) for theta=1
112+ rv = R_left - theta * mathpf . millsratio ( m0 + 0.5 * sigma ) # - R(-d2 )
112113
113- ## Correct pv values for very small sigma/d0
114- idx = (sigma > 0.0 ) & (sigma / np .fmax (1.0 , - d0 ) < 1e-3 ) & (sign > 0 )
114+ ## Small sigma: R(m0-t) - R(m0+t) cancels -> Taylor in sigma (R'''-seeded; cf. bsiv.p2v_sig).
115+ ## The cutoff sigma < 0.037*(1.25 + m0) balances the Taylor truncation against the direct-
116+ ## difference cancellation; outside it the direct R_left - R_right (above) is accurate. (The
117+ ## deep-OTM 1/(x^2+3) asymptotic branch of p2v_sig is omitted -- the Taylor covers all m0.)
118+ idx = (sigma > 0.0 ) & (theta > 0 ) & (sigma < 3.7e-2 * (1.25 + m0 ))
115119 if np .any (idx ):
116- sig_idx = sigma [idx ]
117- d0_idx = d0 [idx ]
118- # Mills ratio derivative:
119- # R'(x) = x R(x) - 1
120- # -R'(-x) = x R(-x) + 1
121- # R'''(x) = x(x^2 + 3) R(x) - (x^2 + 2) = (x^2 + 3) R'(x) + 1
122- # -R'''(-x) = (x^2 + 3) -R'(-x) - 1
123-
124- R_d1 = 1. + d0_idx * MathFuncs .mills_ratio (- d0_idx )
125- # when d0_idx is large negative
126- # R_d1 = 1./(d0_idx**2 + 2.)*(1. - 1./(d0_idx**2 + 4.)*(1. - 5./(d0_idx**2 + 6.))),
127-
128- R_d3 = (d0_idx ** 2 + 3. )* R_d1 - 1.
129- # next term is sig_idx**4/1920 * R_d5
130- rv [idx ] = (R_d1 + sig_idx ** 2 / 24. * R_d3 )* sig_idx
120+ sig = sigma [idx ]
121+ m0i = m0 [idx ]
122+ m0sq = m0i * m0i
123+ # [R(m0-t) - R(m0+t)]/sigma = -R'(m0) + sigma^2/24*(-R''') + ... , t = sigma/2. Seed
124+ # -R'''(m0) directly (cancellation-free), DESCEND to -R'(m0) = (-R'''(m0)+1)/(m0^2+3),
125+ # then ASCEND to -R^(5), -R^(7) via R^(2k+1) = (x^2+4k-1) R^(2k-1) - (2k-1)(2k-2) R^(2k-3).
126+ Rx_d3 = mathpf .millsratio_d3 (m0i ) # -R'''(m0)
127+ Rx_d1 = (Rx_d3 + 1.0 ) / (m0sq + 3.0 ) # -R'(m0)
128+ Rx_d5 = (m0sq + 7.0 )* Rx_d3 - 6.0 * Rx_d1 # -R^(5)(m0)
129+ Rx_d7 = (m0sq + 11.0 )* Rx_d5 - 20.0 * Rx_d3 # -R^(7)(m0)
130+ s2 = sig * sig
131+ rv [idx ] = sig * (Rx_d1 + s2 * (Rx_d3 / 24.0 + s2 * (Rx_d5 / 1920.0 + s2 * Rx_d7 / 322560.0 )))
131132
132133 if type == 0 : # price
133- rv *= spst .norm ._pdf (d0 + sigma / 2. )
134+ rv *= spst .norm ._pdf (- m0 + sigma / 2. )
134135 elif type == 2 : # price-to-delta
135136 rv /= R_left
136137
@@ -148,7 +149,7 @@ def vega(self, strike, spot, texp, cp=1):
148149 vega = df * fwd * spst .norm ._pdf (d1 )* np .sqrt (texp )
149150 return vega
150151
151- def vega2 (self , strike , spot , texp , cp = 1 ):
152+ def vega_d2 (self , strike , spot , texp , cp = 1 ):
152153 """
153154 Second derivative w.r.t. sigma.
154155
@@ -169,11 +170,11 @@ def vega2(self, strike, spot, texp, cp=1):
169170 d1 += 0.5 * sigma_std
170171
171172 # formula according to wikipedia
172- vega2 = df * fwd * spst .norm ._pdf (d1 )* np .sqrt (texp ) * d1 * d2 / sigma_std
173- return vega2
173+ vega_d2 = df * fwd * spst .norm ._pdf (d1 )* np .sqrt (texp ) * d1 * d2 / sigma_std
174+ return vega_d2
174175
175176
176- def d2_var (self , strike , spot , texp , cp = 1 ):
177+ def var_d2 (self , strike , spot , texp , cp = 1 ):
177178 """
178179 2nd derivative w.r.t. variance (=sigma^2)
179180 Eq. (9) in Hull & White (1987)
@@ -200,7 +201,7 @@ def d2_var(self, strike, spot, texp, cp=1):
200201
201202 return risk
202203
203- def d3_var (self , strike , spot , texp , cp = 1 ):
204+ def var_d3 (self , strike , spot , texp , cp = 1 ):
204205 """
205206 3rd derivative w.r.t. variance (=sigma^2)
206207 Eq. (9) in Hull & White (1987)
0 commit comments