def __init__(self, sigma, vov=0, rho=0.0, beta=1.0, intr=0, divr=0): self.sigma = sigma self.vov = vov self.rho = rho self.intr = intr self.divr = divr self.bsm_model = pf.Bsm(sigma, intr=intr, divr=divr)
def test_BsmBasket1Bm(self): ### Check the BsmBasket1Bm price should be same as that of the Bsm price if sigma components are same. for k in range(100): n = np.random.randint(1, 8) spot = np.random.uniform(80, 120, size=n) strike = np.random.uniform(80, 120, size=10) sigma = np.random.uniform(0.01, 1) * np.ones(n) texp = np.random.uniform(0.1, 10) intr = np.random.uniform(0, 0.1) divr = np.random.uniform(0, 0.1) weight = np.random.rand(n) weight /= np.sum(weight) cp = np.where(np.random.rand(10) > 0.5, 1, -1) is_fwd = (np.random.rand() > 0.5) m = pf.BsmBasket1Bm(sigma, weight=weight, intr=intr, divr=divr, is_fwd=is_fwd) p = m.price(strike, spot, texp, cp) m2 = pf.Bsm(sigma[0], intr=intr, divr=divr, is_fwd=is_fwd) p2 = m2.price(strike, np.sum(spot * weight), texp, cp) np.testing.assert_almost_equal(p, p2)
def test_bsm_price(self): bsm = pf.Bsm(sigma=0.2, intr=0.05, divr=0.1) result = bsm.price(strike=np.arange(80, 126, 5), spot=100, texp=1.2) expect_result = np.array([ 15.71361973, 12.46799006, 9.69250803, 7.38869609, 5.52948546, 4.06773495, 2.94558338, 2.10255008, 1.48139131, 1.03159130 ]) np.testing.assert_almost_equal(result, expect_result)
def test_BsmDisp(self): strike = np.arange(80, 126, 5) dbs = pf.BsmDisp(sigma=0.2, beta=1, pivot=125, intr=0.05, divr=0.1) # DBS = BSM if beta=1 bsm = pf.Bsm(sigma=dbs.sigma_disp, intr=0.05, divr=0.1) r1 = bsm.price(strike, 100, 2.5, cp=-1) r2 = dbs.price(strike, 100, 2.5, cp=-1) np.testing.assert_almost_equal(r1, r2) # DBS = Norm if beta=0 dbs.beta = 0.0001 dbs.is_fwd = True norm = pf.Norm(sigma=dbs.sigma_disp * dbs.pivot, intr=0.05, divr=0.1, is_fwd=True) r1 = norm.price(strike, 100, 2.5, cp=-1) r2 = dbs.price(strike, 100, 2.5, cp=-1) np.testing.assert_almost_equal(r1 / r2, 1, decimal=4) dbs.is_fwd = False # Approximate BSM vol dbs.beta = 0.2 v1 = dbs.vol_smile(strike, 100, 2.5, model='bsm') v2 = dbs.vol_smile(strike, 100, 2.5, model='bsm-approx') np.testing.assert_almost_equal(v1 / v2, 1, decimal=4) p1 = dbs.price(strike, 100, 2.5) p2 = pf.Bsm(v1, intr=0.05, divr=0.1).price(strike, 100, 2.5) np.testing.assert_almost_equal(p1, p2) # Approximate Bachelier vol dbs.beta = 0.8 v1 = dbs.vol_smile(strike, 100, 2.5, model='norm') v2 = dbs.vol_smile(strike, 100, 2.5, model='norm-approx') np.testing.assert_almost_equal(v1 / v2, 1, decimal=4) p1 = dbs.price(strike, 100, 2.5) p2 = pf.Norm(v1, intr=0.05, divr=0.1).price(strike, 100, 2.5) np.testing.assert_almost_equal(p1, p2)
def test_bsm_iv_greeks(self): for k in range(100): spot = np.random.uniform(80, 120) strike = np.random.uniform(80, 120) sigma = np.random.uniform(0.1, 10) texp = np.random.uniform(0.1, 10) intr = np.random.uniform(0, 0.3) divr = np.random.uniform(0, 0.3) cp = 1 if np.random.rand() > 0.5 else -1 is_fwd = (np.random.rand() > 0.5) # print( spot, strike, vol, texp, intr, divr, cp) m_bsm = pf.Bsm(sigma, intr=intr, divr=divr, is_fwd=is_fwd) price = m_bsm.price(strike, spot, texp, cp), # get implied vol iv = m_bsm.impvol(price, strike, spot, texp=texp, cp=cp) # now price option with the obtained implied vol m_bsm2 = copy.copy(m_bsm) m_bsm2.sigma = iv price_imp = m_bsm2.price(strike, spot, texp, cp) # compare the two prices self.assertAlmostEqual(price, price_imp, delta=200 * m_bsm.IMPVOL_TOL) delta1 = m_bsm.delta(strike=strike, spot=spot, texp=texp, cp=cp) delta2 = m_bsm.delta_numeric(strike=strike, spot=spot, texp=texp, cp=cp) self.assertAlmostEqual(delta1, delta2, delta=1e-4) gamma1 = m_bsm.delta(strike=strike, spot=spot, texp=texp, cp=cp) gamma2 = m_bsm.delta_numeric(strike=strike, spot=spot, texp=texp, cp=cp) self.assertAlmostEqual(gamma1, gamma2, delta=1e-4) vega1 = m_bsm.vega(strike=strike, spot=spot, texp=texp, cp=cp) vega2 = m_bsm.vega_numeric(strike=strike, spot=spot, texp=texp, cp=cp) self.assertAlmostEqual(vega1, vega2, delta=1e-3)
class ModelBsmMC: beta = 1.0 # fixed (not used) vov, rho = 0.0, 0.0 sigma, intr, divr = None, None, None bsm_model = None ''' You may define more members for MC: time step, etc ''' def __init__(self, sigma, vov=0, rho=0.0, beta=1.0, intr=0, divr=0, time_steps=1_000, n_samples=10_000): self.sigma = sigma self.vov = vov self.rho = rho self.intr = intr self.divr = divr self.time_steps = time_steps self.n_samples = n_samples self.bsm_model = pf.Bsm(sigma, intr=intr, divr=divr)
def price( self, strike, spot, texp, sigma, delta, intr=0, divr=0, psi_c=1.5, path=10000, scheme="QE", seed=None, ): """ Conditional MC routine for 3/2 model Generate paths for vol only using QE discretization scheme. Compute integrated variance and get BSM prices vector for all strikes. Args: strike: strike price, in vector form spot: spot (or forward) texp: time to expiry sigma: initial volatility delta: length of each time step intr: interest rate (domestic interest rate) divr: dividend/convenience yield (foreign interest rate) psi_c: critical value for psi, lying in [1, 2] path: number of vol paths generated scheme: discretization scheme for vt, {'QE', 'TG', 'Euler', 'Milstein', 'KJ'} seed: random seed for rv generation Return: BSM price vector for all strikes """ self.sigma = sigma self.bsm_model = pf.Bsm(self.sigma, intr=intr, divr=divr) self.delta = delta self.path = int(path) self.step = int(texp / self.delta) # xt = 1 / vt xt = 1 / self.sigma**2 * np.ones([self.path, self.step + 1]) np.random.seed(seed) # equivalent kappa and theta for xt to follow a Heston model kappa_new = self.kappa * self.theta theta_new = (self.kappa + self.vov**2) / (self.kappa * self.theta) vov_new = -self.vov if scheme == "QE": u = np.random.uniform(size=(self.path, self.step)) expo = np.exp(-kappa_new * self.delta) for i in range(self.step): # compute m, s_square, psi given xt(i) m = theta_new + (xt[:, i] - theta_new) * expo s2 = xt[:, i] * (vov_new**2) * expo * ( 1 - expo) / kappa_new + theta_new * (vov_new**2) * ( (1 - expo)**2) / (2 * kappa_new) psi = s2 / m**2 # compute xt(i+1) given psi below = np.where(psi <= psi_c)[0] ins = 2 * psi[below]**-1 b2 = ins - 1 + np.sqrt(ins * (ins - 1)) b = np.sqrt(b2) a = m[below] / (1 + b2) z = spst.norm.ppf(u[below, i]) xt[below, i + 1] = a * (b + z)**2 above = np.where(psi > psi_c)[0] p = (psi[above] - 1) / (psi[above] + 1) beta = (1 - p) / m[above] for k in range(len(above)): if u[above[k], i] > p[k]: xt[above[k], i + 1] = beta[k]**-1 * np.log( (1 - p[k]) / (1 - u[above[k], i])) else: xt[above[k], i + 1] = 0 elif scheme == "TG": if np.all(self.rx_results) == None: self.psi_points, self.rx_results = self.prepare_rx() expo = np.exp(-self.kappa * self.delta) for i in range(self.step): # compute m, s_square, psi given vt(i) m = theta_new + (xt[:, i] - theta_new) * expo s2 = xt[:, i] * (vov_new**2) * expo * ( 1 - expo) / kappa_new + theta_new * (vov_new**2) * ( (1 - expo)**2) / (2 * kappa_new) psi = s2 / m**2 rx = np.array([self.find_rx(j) for j in psi]) z = np.random.normal(size=(self.path, self.step)) mu_v = np.zeros_like(z) sigma_v = np.zeros_like(z) mu_v[:, i] = rx * m / (spst.norm.pdf(rx) + rx * spst.norm.cdf(rx)) sigma_v[:, i] = (np.sqrt(s2) * psi**(-0.5) / (spst.norm.pdf(rx) + rx * spst.norm.cdf(rx))) xt[:, i + 1] = np.fmax(mu_v[:, i] + sigma_v[:, i] * z[:, i], 0) elif scheme == "Euler": z = np.random.normal(size=(self.path, self.step)) for i in range(self.step): xt[:, i + 1] = (xt[:, i] + kappa_new * (theta_new - np.max(xt[:, i], 0)) * self.delta + vov_new * np.sqrt(np.max(xt[:, i], 0) * self.delta) * z[:, i]) elif scheme == "Milstein": z = np.random.normal(size=(self.path, self.step)) for i in range(self.step): xt[:, i + 1] = (xt[:, i] + kappa_new * (theta_new - np.max(xt[:, i], 0)) * self.delta + vov_new * np.sqrt(np.max(xt[:, i], 0) * self.delta) * z[:, i] + vov_new**2 * 0.25 * (z[:, i]**2 - 1) * self.delta) elif scheme == "KJ": z = np.random.normal(size=(self.path, self.step)) for i in range(self.step): xt[:, i + 1] = (xt[:, i] + kappa_new * theta_new * self.delta + vov_new * np.sqrt(np.max(xt[:, i], 0) * self.delta) * z[:, i] + vov_new**2 * 0.25 * (z[:, i]**2 - 1) * self.delta) / ( 1 + kappa_new * self.delta) # compute integral of vt, equivalent spot and vol vt = 1 / xt below_0 = np.where(vt < 0) vt[below_0] = 0 vt_int = spint.simps(vt, dx=self.delta) spot_cmc = spot * np.exp(self.rho / self.vov * (np.log(vt[:, -1] / vt[:, 0]) - self.kappa * (self.theta * texp - vt_int * (1 + self.vov**2 * 0.5 / self.kappa))) - self.rho**2 * vt_int / 2) vol_cmc = np.sqrt((1 - self.rho**2) * vt_int / texp) # compute bsm price vector for the given strike vector price_cmc = np.zeros_like(strike) for j in range(len(strike)): price_cmc[j] = np.mean( self.bsm_model.price_formula(strike[j], spot_cmc, vol_cmc, texp, intr=intr, divr=divr)) return price_cmc