Skip to content

Commit 21dc2fb

Browse files
committed
improved
1 parent 797440d commit 21dc2fb

File tree

1 file changed

+71
-90
lines changed

1 file changed

+71
-90
lines changed

pyfeng/sabr_mc.py

Lines changed: 71 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class SabrCondMc(sabr.SabrABC, sv.CondMcBsmABC):
1818

1919
def vol_paths(self, tobs, mu=0):
2020
"""
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)
2222
2323
Args:
2424
tobs: observation time (array)
@@ -29,12 +29,12 @@ def vol_paths(self, tobs, mu=0):
2929

3030
texp = tobs[-1]
3131
tobs01 = tobs / texp # normalized time: 0<s<1
32-
vov_std = self.vov * np.sqrt(texp)
32+
vovn = self.vov * np.sqrt(texp)
3333

3434
log_sig_s = self._bm_incr(tobs01, cum=True) # B_s (0 <= s <= 1)
3535
log_rn_deriv = 0.0 if mu == 0 else -mu * (log_sig_s[-1, :] + 0.5 * mu)
3636

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])
3838
log_sig_s = np.insert(log_sig_s, 0, np.zeros(log_sig_s.shape[1]), axis=0)
3939
return np.exp(log_sig_s), log_rn_deriv
4040

@@ -87,10 +87,10 @@ def mass_zero(self, spot, texp, log=False, mu=0):
8787
* np.power(spot, 1.0 - self.beta)
8888
/ (self.sigma * (1.0 - self.beta))
8989
)
90-
vov_std = self.vov * np.sqrt(texp)
90+
vovn = self.vov * np.sqrt(texp)
9191

9292
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)
9494
# print(f'mu = {mu}')
9595

9696
fwd_cond, vol_cond, log_rn_deriv = self.cond_spot_sigma(texp, mu=mu)
@@ -117,32 +117,35 @@ class SabrMcExactCai2017(sabr.SabrABC, sv.CondMcBsmABC):
117117
- 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
118118
"""
119119
m_inv = 20
120-
n_inv = 35
120+
m_euler = 20
121+
n_euler = 35
121122
comb_coef = None
122123
nn = None
123124

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):
125126
"""
126127
Set MC parameters
127128
128129
Args:
129130
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)
132134
rn_seed: random number seed
133135
antithetic: antithetic
134136
"""
135137
self.n_path = int(n_path)
136138
self.m_inv = m_inv
137-
self.n_inv = n_inv
139+
self.m_euler = m_euler
140+
self.n_euler = n_euler
138141
self.dt = None
139142
self.rn_seed = rn_seed
140143
self.antithetic = antithetic
141144
self.rn_seed = rn_seed
142145
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)
144147
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)
146149

147150
def vol_paths(self, tobs, mu=0):
148151
return None
@@ -158,8 +161,12 @@ def sigma_final(self, vovn):
158161
-------
159162
vol at maturity
160163
"""
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)
163170

164171
sigma_T = np.exp(vovn * (zz - vovn/2))
165172
return sigma_T
@@ -186,67 +193,67 @@ def cond_laplace(self, theta, vovn, sigma_T):
186193
return np.exp((x**2 - phi**2) / (2*vovn**2)) / theta
187194

188195
def inv_laplace(self, u, vovn, sigma_T):
189-
'''
196+
"""
190197
Eq. (16) in the article
191198
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+
"""
201208

202209
## index from 0 to m + n
203210
ss_j = self.cond_laplace((self.m_inv - 2j * np.pi * self.nn) / (2*u), vovn, sigma_T).real
204211
term1 = 0.5 * ss_j[0]
205212
ss_j[1::2] *= -1
206213
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:])
208215

209216
origin_L = np.exp(self.m_inv/2) / u * (-term1 + term2)
210217

211218
return origin_L
212219

213-
def conditional_state(self, vovn):
220+
def cond_int_var(self, vovn, sigma_final):
214221
"""
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
224230
"""
225231

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)
228237

229238
int_var = np.zeros(self.n_path)
230-
sigma_final = self.sigma_final(vovn)
231239

232240
for i in range(self.n_path):
233241
obj_func = lambda x: self.inv_laplace(x, vovn, sigma_final[i]) - u_rn[i]
234242

235-
if sigma_final[i] > 1:
236-
x0 = 1/sigma_final[i]
237-
else:
238-
x0 = 2/(1+sigma_final[i])
239-
240243
sol = spop.brentq(obj_func, 0.000001, 100)
241244
int_var[i] = 1 / sol
242-
return sigma_final, int_var
245+
246+
return int_var
243247

244248
def cond_spot_sigma(self, texp):
245249

246250
rhoc = np.sqrt(1.0 - self.rho ** 2)
247251
rho_sigma = self.rho * self.sigma
248252
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))
250257

251258
vol_cond = rhoc * np.sqrt(int_var)
252259
if np.isclose(self.beta, 0):
@@ -257,6 +264,25 @@ def cond_spot_sigma(self, texp):
257264
)
258265
return fwd_cond, vol_cond
259266

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+
260286
# The algorithem below is about pricing when 0<=beta<1
261287
def simu_ST(self, beta, VT, spot):
262288
'''
@@ -404,48 +430,3 @@ def theta_func(mu2, yita, s):
404430
+ 2 * theta_s(s) / sigma) ** 0.5
405431
cdf = spst.norm.cdf(z)
406432
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

Comments
 (0)