def heston_calibration(df_option, ival=None): """ calibrate heston model """ # extract rates and div yields from the data set df_tmp = DataFrame.filter(df_option, items=["dtExpiry", "iRate", "iDiv"]) grouped = df_tmp.groupby("dtExpiry") df_rates = grouped.agg(lambda x: x[0]) dtTrade = df_option["dtTrade"][0] # back out the spot from any forward iRate = df_option["iRate"][0] iDiv = df_option["iDiv"][0] TTM = df_option["TTM"][0] Fwd = df_option["Fwd"][0] spot = SimpleQuote(Fwd * np.exp(-(iRate - iDiv) * TTM)) print("Spot: %f risk-free rate: %f div. yield: %f" % (spot.value, iRate, iDiv)) # build array of option helpers hh = heston_helpers(spot, df_option, dtTrade, df_rates) options = hh["options"] spot = hh["spot"] risk_free_ts = dfToZeroCurve(df_rates["iRate"], dtTrade) dividend_ts = dfToZeroCurve(df_rates["iDiv"], dtTrade) # initial values for parameters if ival is None: ival = {"v0": 0.1, "kappa": 1.0, "theta": 0.1, "sigma": 0.5, "rho": -0.5} process = HestonProcess( risk_free_ts, dividend_ts, spot, ival["v0"], ival["kappa"], ival["theta"], ival["sigma"], ival["rho"] ) model = HestonModel(process) engine = AnalyticHestonEngine(model, 64) for option in options: option.set_pricing_engine(engine) om = LevenbergMarquardt(1e-8, 1e-8, 1e-8) model.calibrate(options, om, EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8)) print("model calibration results:") print("v0: %f kappa: %f theta: %f sigma: %f rho: %f" % (model.v0, model.kappa, model.theta, model.sigma, model.rho)) calib_error = (1.0 / len(options)) * sum([pow(o.calibration_error() * 100.0, 2) for o in options]) print("SSE: %f" % calib_error) # merge the fitted volatility and the input data set return merge_df(df_option, options, "Heston")
def test_smith(self): # test against result published in # Journal of Computational Finance Vol. 11/1 Fall 2007 # An almost exact simulation method for the heston model settlement_date = today() self.settings.evaluation_date = settlement_date daycounter = ActualActual() timeToMaturity = 4 exercise_date = settlement_date + timeToMaturity * 365 c_payoff = PlainVanillaPayoff(Call, 100) exercise = EuropeanExercise(exercise_date) risk_free_ts = flat_rate(0., daycounter) dividend_ts = flat_rate(0., daycounter) s0 = SimpleQuote(100.0) v0 = 0.0194 kappa = 1.0407 theta = 0.0586 sigma = 0.5196 rho = -.6747 nb_steps_a = 100 nb_paths = 20000 seed = 12347 process = HestonProcess(risk_free_ts, dividend_ts, s0, v0, kappa, theta, sigma, rho, QUADRATICEXPONENTIAL) model = HestonModel(process) option = VanillaOption(c_payoff, exercise) engine = AnalyticHestonEngine(model, 144) option.set_pricing_engine(engine) price_fft = option.net_present_value engine = MCEuropeanHestonEngine(process, antithetic_variate=True, steps_per_year=nb_steps_a, required_samples=nb_paths, seed=seed) option.set_pricing_engine(engine) price_mc = option.net_present_value expected = 15.1796 tolerance = .05 self.assertAlmostEqual(price_fft, expected, delta=tolerance) self.assertAlmostEqual(price_mc, expected, delta=tolerance)
def heston_calibration(df_option, dtTrade=None, df_rates=None, ival=None): """ calibrate heston model """ # array of option helpers print df_option, df_rates, ival hh = heston_helpers(df_option, dtTrade, df_rates, ival) options = hh['options'] spot = hh['spot'] risk_free_ts = df_to_zero_curve(df_rates['R'], dtTrade) dividend_ts = df_to_zero_curve(df_rates['D'], dtTrade) if ival is None: ival = { 'v0': 0.1, 'kappa': 1.0, 'theta': 0.1, 'sigma': 0.5, 'rho': -.5 } process = HestonProcess(risk_free_ts, dividend_ts, spot, ival['v0'], ival['kappa'], ival['theta'], ival['sigma'], ival['rho']) model = HestonModel(process) engine = AnalyticHestonEngine(model, 64) for option in options: option.set_pricing_engine(engine) om = LevenbergMarquardt(1e-8, 1e-8, 1e-8) model.calibrate(options, om, EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8)) print('model calibration results:') print('v0: %f kappa: %f theta: %f sigma: %f rho: %f' % (model.v0, model.kappa, model.theta, model.sigma, model.rho)) calib_error = (1.0 / len(options)) * sum( [pow(o.calibration_error() * 100.0, 2) for o in options]) print('SSE: %f' % calib_error) return merge_df(df_option, options, 'Heston')
def heston_calibration(df_option, ival=None): """ calibrate heston model """ tmp = make_helpers(df_option) risk_free_ts = tmp['risk_free_rate'] dividend_ts = tmp['dividend_rate'] spot = tmp['spot'] options = tmp['options'] # initial values for parameters if ival is None: ival = { 'v0': 0.1, 'kappa': 1.0, 'theta': 0.1, 'sigma': 0.5, 'rho': -.5 } process = HestonProcess(risk_free_ts, dividend_ts, spot, ival['v0'], ival['kappa'], ival['theta'], ival['sigma'], ival['rho']) model = HestonModel(process) engine = AnalyticHestonEngine(model, 64) for option in options: option.set_pricing_engine(engine) om = LevenbergMarquardt(1e-8, 1e-8, 1e-8) model.calibrate(options, om, EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8)) print('model calibration results:') print('v0: %f kappa: %f theta: %f sigma: %f rho: %f' % (model.v0, model.kappa, model.theta, model.sigma, model.rho)) calib_error = (1.0 / len(options)) * sum( [pow(o.calibration_error() * 100.0, 2) for o in options]) print('SSE: %f' % calib_error) # merge the fitted volatility and the input data set return merge_df(df_option, options, 'Heston')
def heston_calibration(df_option, dtTrade=None, df_rates=None, ival=None): """ calibrate heston model """ # array of option helpers print df_option, df_rates, ival hh = heston_helpers(df_option, dtTrade, df_rates, ival) options = hh['options'] spot = hh['spot'] risk_free_ts = dfToZeroCurve(df_rates['R'], dtTrade) dividend_ts = dfToZeroCurve(df_rates['D'], dtTrade) if ival is None: ival = {'v0': 0.1, 'kappa': 1.0, 'theta': 0.1, 'sigma': 0.5, 'rho': -.5} process = HestonProcess( risk_free_ts, dividend_ts, spot, ival['v0'], ival['kappa'], ival['theta'], ival['sigma'], ival['rho']) model = HestonModel(process) engine = AnalyticHestonEngine(model, 64) for option in options: option.set_pricing_engine(engine) om = LevenbergMarquardt(1e-8, 1e-8, 1e-8) model.calibrate( options, om, EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8) ) print('model calibration results:') print('v0: %f kappa: %f theta: %f sigma: %f rho: %f' % (model.v0, model.kappa, model.theta, model.sigma, model.rho)) calib_error = (1.0/len(options)) * sum( [pow(o.calibration_error()*100.0,2) for o in options]) print('SSE: %f' % calib_error) return merge_df(df_option, options, 'Heston')
def heston_calibration(df_option, ival=None): """ calibrate heston model """ tmp = make_helpers(df_option) risk_free_ts = tmp['risk_free_rate'] dividend_ts = tmp['dividend_rate'] spot = tmp['spot'] options = tmp['options'] # initial values for parameters if ival is None: ival = {'v0': 0.1, 'kappa': 1.0, 'theta': 0.1, 'sigma': 0.5, 'rho': -.5} process = HestonProcess( risk_free_ts, dividend_ts, spot, ival['v0'], ival['kappa'], ival['theta'], ival['sigma'], ival['rho']) model = HestonModel(process) engine = AnalyticHestonEngine(model, 64) for option in options: option.set_pricing_engine(engine) om = LevenbergMarquardt(1e-8, 1e-8, 1e-8) model.calibrate( options, om, EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8) ) print('model calibration results:') print('v0: %f kappa: %f theta: %f sigma: %f rho: %f' % (model.v0, model.kappa, model.theta, model.sigma, model.rho)) calib_error = (1.0 / len(options)) * sum( [pow(o.calibration_error() * 100.0, 2) for o in options]) print('SSE: %f' % calib_error) # merge the fitted volatility and the input data set return merge_df(df_option, options, 'Heston')
def test_analytic_versus_black(self): settlement_date = today() self.settings.evaluation_date = settlement_date daycounter = ActualActual() exercise_date = settlement_date + 6 * Months payoff = PlainVanillaPayoff(Put, 30) exercise = EuropeanExercise(exercise_date) risk_free_ts = flat_rate(0.1, daycounter) dividend_ts = flat_rate(0.04, daycounter) s0 = SimpleQuote(32.0) v0 = 0.05 kappa = 5.0 theta = 0.05 sigma = 1.0e-4 rho = 0.0 process = HestonProcess( risk_free_ts, dividend_ts, s0, v0, kappa, theta, sigma, rho ) option = VanillaOption(payoff, exercise) engine = AnalyticHestonEngine(HestonModel(process), 144) option.set_pricing_engine(engine) calculated = option.net_present_value year_fraction = daycounter.year_fraction( settlement_date, exercise_date ) forward_price = 32 * np.exp((0.1 - 0.04) * year_fraction) expected = blackFormula( payoff.type, payoff.strike, forward_price, np.sqrt(0.05 * year_fraction) ) * np.exp(-0.1 * year_fraction) tolerance = 2.0e-7 self.assertAlmostEqual( calculated, expected, delta=tolerance )
def heston_pricer(trade_date, options, params, rates, spot): """ Price a list of European options with heston model. """ spot = SimpleQuote(spot) risk_free_ts = df_to_zero_curve(rates[nm.INTEREST_RATE], trade_date) dividend_ts = df_to_zero_curve(rates[nm.DIVIDEND_YIELD], trade_date) process = HestonProcess(risk_free_ts, dividend_ts, spot, **params) model = HestonModel(process) engine = AnalyticHestonEngine(model, 64) settlement_date = pydate_to_qldate(trade_date) settings = Settings() settings.evaluation_date = settlement_date modeled_values = np.zeros(len(options)) for index, row in options.T.iteritems(): expiry_date = row[nm.EXPIRY_DATE] strike = row[nm.STRIKE] option_type = Call if row[nm.OPTION_TYPE] == nm.CALL_OPTION else Put payoff = PlainVanillaPayoff(option_type, strike) expiry_qldate = pydate_to_qldate(expiry_date) exercise = EuropeanExercise(expiry_qldate) option = VanillaOption(payoff, exercise) option.set_pricing_engine(engine) modeled_values[index] = option.net_present_value prices = options.filter( items=[nm.EXPIRY_DATE, nm.STRIKE, nm.OPTION_TYPE, nm.SPOT]) prices[nm.PRICE] = modeled_values prices[nm.TRADE_DATE] = trade_date return prices
def test_simulate_heston_2(self): s0 = SimpleQuote(100.0) v0 = 0.05 kappa = 5.0 theta = 0.05 sigma = 1.0e-4 rho = 0.0 process = HestonProcess(self.risk_free_ts, self.dividend_ts, s0, v0, kappa, theta, sigma, rho) model = HestonModel(process) nbPaths = 4 nbSteps = 100 horizon = 1 seed = 12345 res = simulateHeston(model, nbPaths, nbSteps, horizon, seed) self.assertAlmostEqual(res[1, -1], 152.50, delta=.1)
def test_simulate_heston_1(self): settings = self.settings settlement_date = today() settings.evaluation_date = settlement_date # simulate Heston paths paths = 4 steps = 10 horizon = 1 seed = 12345 model = HestonModel(self.heston_process) res = simulate_model(model, paths, steps, horizon, seed) time = res[0, :] time_expected = np.arange(0, 1.1, .1) simulations = res[1:, :].T np.testing.assert_array_almost_equal(time, time_expected, decimal=4)
def test_heston_hw_calibration(self): """ From Quantlib test suite """ print("Testing Heston Hull-White calibration...") ## Calibration of a hybrid Heston-Hull-White model using ## the finite difference HestonHullWhite pricing engine ## Input surface is based on a Heston-Hull-White model with ## Hull-White: a = 0.00883, \sigma = 0.00631 ## Heston : \nu = 0.12, \kappa = 2.0, ## \theta = 0.09, \sigma = 0.5, \rho=-0.75 ## Equity Short rate correlation: -0.5 dc = Actual365Fixed() calendar = TARGET() todays_date = Date(28, March, 2004) settings = Settings() settings.evaluation_date = todays_date r_ts = flat_rate(todays_date, 0.05, dc) ## assuming, that the Hull-White process is already calibrated ## on a given set of pure interest rate calibration instruments. hw_process = HullWhiteProcess(r_ts, a=0.00883, sigma=0.00631) q_ts = flat_rate(todays_date, 0.02, dc) s0 = SimpleQuote(100.0) # vol surface strikes = [50, 75, 90, 100, 110, 125, 150, 200] maturities = [1 / 12., 3 / 12., 0.5, 1.0, 2.0, 3.0, 5.0, 7.5, 10] vol = [ 0.482627, 0.407617, 0.366682, 0.340110, 0.314266, 0.280241, 0.252471, 0.325552, 0.464811, 0.393336, 0.354664, 0.329758, 0.305668, 0.273563, 0.244024, 0.244886, 0.441864, 0.375618, 0.340464, 0.318249, 0.297127, 0.268839, 0.237972, 0.225553, 0.407506, 0.351125, 0.322571, 0.305173, 0.289034, 0.267361, 0.239315, 0.213761, 0.366761, 0.326166, 0.306764, 0.295279, 0.284765, 0.270592, 0.250702, 0.222928, 0.345671, 0.314748, 0.300259, 0.291744, 0.283971, 0.273475, 0.258503, 0.235683, 0.324512, 0.303631, 0.293981, 0.288338, 0.283193, 0.276248, 0.266271, 0.250506, 0.311278, 0.296340, 0.289481, 0.285482, 0.281840, 0.276924, 0.269856, 0.258609, 0.303219, 0.291534, 0.286187, 0.283073, 0.280239, 0.276414, 0.270926, 0.262173 ] start_v0 = 0.2 * 0.2 start_theta = start_v0 start_kappa = 0.5 start_sigma = 0.25 start_rho = -0.5 equityShortRateCorr = -0.5 corrConstraint = HestonHullWhiteCorrelationConstraint( equityShortRateCorr) heston_process = HestonProcess(r_ts, q_ts, s0, start_v0, start_kappa, start_theta, start_sigma, start_rho) h_model = HestonModel(heston_process) h_engine = AnalyticHestonEngine(h_model) options = [] # first calibrate a heston model to get good initial # parameters for i in range(len(maturities)): maturity = Period(int(maturities[i] * 12.0 + 0.5), Months) for j, s in enumerate(strikes): v = SimpleQuote(vol[i * len(strikes) + j]) helper = HestonModelHelper(maturity, calendar, s0.value, s, v, r_ts, q_ts, PriceError) helper.set_pricing_engine(h_engine) options.append(helper) om = LevenbergMarquardt(1e-6, 1e-8, 1e-8) # Heston model h_model.calibrate(options, om, EndCriteria(400, 40, 1.0e-8, 1.0e-4, 1.0e-8)) print("Heston calibration") print("v0: %f" % h_model.v0) print("theta: %f" % h_model.theta) print("kappa: %f" % h_model.kappa) print("sigma: %f" % h_model.sigma) print("rho: %f" % h_model.rho) h_process_2 = HestonProcess(r_ts, q_ts, s0, h_model.v0, h_model.kappa, h_model.theta, h_model.sigma, h_model.rho) hhw_model = HestonModel(h_process_2) options = [] for i in range(len(maturities)): tGrid = np.max((10.0, maturities[i] * 10.0)) hhw_engine = FdHestonHullWhiteVanillaEngine( hhw_model, hw_process, equityShortRateCorr, tGrid, 61, 13, 9, 0, True, FdmSchemeDesc.Hundsdorfer()) hhw_engine.enable_multiple_strikes_caching(strikes) maturity = Period(int(maturities[i] * 12.0 + 0.5), Months) # multiple strikes engine works best if the first option # per maturity has the average strike (because the first # option is priced first during the calibration and # the first pricing is used to calculate the prices # for all strikes # list of strikes by distance from moneyness indx = np.argsort(np.abs(np.array(strikes) - s0.value)) for j, tmp in enumerate(indx): js = indx[j] s = strikes[js] v = SimpleQuote(vol[i * len(strikes) + js]) helper = HestonModelHelper(maturity, calendar, s0.value, strikes[js], v, r_ts, q_ts, PriceError) helper.set_pricing_engine(hhw_engine) options.append(helper) vm = LevenbergMarquardt(1e-6, 1e-2, 1e-2) hhw_model.calibrate(options, vm, EndCriteria(400, 40, 1.0e-8, 1.0e-4, 1.0e-8), corrConstraint) print("Heston HW calibration with FD engine") print("v0: %f" % hhw_model.v0) print("theta: %f" % hhw_model.theta) print("kappa: %f" % hhw_model.kappa) print("sigma: %f" % hhw_model.sigma) print("rho: %f" % hhw_model.rho) relTol = 0.05 expected_v0 = 0.12 expected_kappa = 2.0 expected_theta = 0.09 expected_sigma = 0.5 expected_rho = -0.75 self.assertAlmostEquals(np.abs(hhw_model.v0 - expected_v0) / expected_v0, 0, delta=relTol) self.assertAlmostEquals(np.abs(hhw_model.theta - expected_theta) / expected_theta, 0, delta=relTol) self.assertAlmostEquals(np.abs(hhw_model.kappa - expected_kappa) / expected_kappa, 0, delta=relTol) self.assertAlmostEquals(np.abs(hhw_model.sigma - expected_sigma) / expected_sigma, 0, delta=relTol) self.assertAlmostEquals(np.abs(hhw_model.rho - expected_rho) / expected_rho, 0, delta=relTol)
def test_black_calibration(self): # calibrate a Heston model to a constant volatility surface without # smile. expected result is a vanishing volatility of the volatility. # In addition theta and v0 should be equal to the constant variance todays_date = today() self.settings.evaluation_date = todays_date daycounter = Actual360() calendar = NullCalendar() risk_free_ts = flat_rate(0.04, daycounter) dividend_ts = flat_rate(0.50, daycounter) option_maturities = [ Period(1, Months), Period(2, Months), Period(3, Months), Period(6, Months), Period(9, Months), Period(1, Years), Period(2, Years) ] options = [] s0 = SimpleQuote(1.0) vol = SimpleQuote(0.1) volatility = vol.value for maturity in option_maturities: for moneyness in np.arange(-1.0, 2.0, 1.): tau = daycounter.year_fraction( risk_free_ts.reference_date, calendar.advance( risk_free_ts.reference_date, period=maturity) ) forward_price = s0.value * dividend_ts.discount(tau) / \ risk_free_ts.discount(tau) strike_price = forward_price * np.exp( -moneyness * volatility * np.sqrt(tau) ) options.append( HestonModelHelper( maturity, calendar, s0.value, strike_price, vol, risk_free_ts, dividend_ts ) ) for sigma in np.arange(0.1, 0.7, 0.2): v0 = 0.01 kappa = 0.2 theta = 0.02 rho = -0.75 process = HestonProcess( risk_free_ts, dividend_ts, s0, v0, kappa, theta, sigma, rho ) self.assertEqual(v0, process.v0) self.assertEqual(kappa, process.kappa) self.assertEqual(theta, process.theta) self.assertEqual(sigma, process.sigma) self.assertEqual(rho, process.rho) self.assertEqual(1.0, process.s0().value) model = HestonModel(process) engine = AnalyticHestonEngine(model, 96) for option in options: option.set_pricing_engine(engine) optimisation_method = LevenbergMarquardt(1e-8, 1e-8, 1e-8) end_criteria = EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8) model.calibrate(options, optimisation_method, end_criteria) tolerance = 3.0e-3 self.assertFalse(model.sigma > tolerance) self.assertAlmostEqual( model.kappa * model.theta, model.kappa * volatility ** 2, delta=tolerance ) self.assertAlmostEqual(model.v0, volatility ** 2, delta=tolerance)
def test_DAX_calibration(self): # this example is taken from A. Sepp # Pricing European-Style Options under Jump Diffusion Processes # with Stochstic Volatility: Applications of Fourier Transform # http://math.ut.ee/~spartak/papers/stochjumpvols.pdf settlement_date = Date(5, July, 2002) self.settings.evaluation_date = settlement_date daycounter = Actual365Fixed() calendar = TARGET() t = [13, 41, 75, 165, 256, 345, 524, 703] r = [0.0357,0.0349,0.0341,0.0355,0.0359,0.0368,0.0386,0.0401] dates = [settlement_date] + [settlement_date + val for val in t] rates = [0.0357] + r risk_free_ts = ZeroCurve(dates, rates, daycounter) dividend_ts = FlatForward( settlement_date, forward=0.0, daycounter=daycounter ) v = [ 0.6625,0.4875,0.4204,0.3667,0.3431,0.3267,0.3121,0.3121, 0.6007,0.4543,0.3967,0.3511,0.3279,0.3154,0.2984,0.2921, 0.5084,0.4221,0.3718,0.3327,0.3155,0.3027,0.2919,0.2889, 0.4541,0.3869,0.3492,0.3149,0.2963,0.2926,0.2819,0.2800, 0.4060,0.3607,0.3330,0.2999,0.2887,0.2811,0.2751,0.2775, 0.3726,0.3396,0.3108,0.2781,0.2788,0.2722,0.2661,0.2686, 0.3550,0.3277,0.3012,0.2781,0.2781,0.2661,0.2661,0.2681, 0.3428,0.3209,0.2958,0.2740,0.2688,0.2627,0.2580,0.2620, 0.3302,0.3062,0.2799,0.2631,0.2573,0.2533,0.2504,0.2544, 0.3343,0.2959,0.2705,0.2540,0.2504,0.2464,0.2448,0.2462, 0.3460,0.2845,0.2624,0.2463,0.2425,0.2385,0.2373,0.2422, 0.3857,0.2860,0.2578,0.2399,0.2357,0.2327,0.2312,0.2351, 0.3976,0.2860,0.2607,0.2356,0.2297,0.2268,0.2241,0.2320 ] s0 = SimpleQuote(4468.17) strikes = [ 3400, 3600, 3800, 4000, 4200, 4400, 4500, 4600, 4800, 5000, 5200, 5400, 5600 ] options = [] for s, strike in enumerate(strikes): for m in range(len(t)): vol = SimpleQuote(v[s * 8 + m]) # round to weeks maturity = Period((int)((t[m] + 3) / 7.), Weeks) options.append( HestonModelHelper( maturity, calendar, s0.value, strike, vol, risk_free_ts, dividend_ts, ImpliedVolError ) ) v0 = 0.1 kappa = 1.0 theta = 0.1 sigma = 0.5 rho = -0.5 process = HestonProcess( risk_free_ts, dividend_ts, s0, v0, kappa, theta, sigma, rho ) model = HestonModel(process) engine = AnalyticHestonEngine(model, 64) for option in options: option.set_pricing_engine(engine) om = LevenbergMarquardt(1e-8, 1e-8, 1e-8) model.calibrate( options, om, EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8) ) sse = 0 for i in range(len(strikes) * len(t)): diff = options[i].calibration_error() * 100.0 sse += diff * diff expected = 177.2 # see article by A. Sepp. self.assertAlmostEqual(expected, sse, delta=1.0)
def heston_calibration(df_option, ival=None): """ calibrate heston model """ # extract rates and div yields from the data set df_tmp = DataFrame.filter(df_option, items=['dtExpiry', 'iRate', 'iDiv']) grouped = df_tmp.groupby('dtExpiry') df_rates = grouped.agg(lambda x: x[0]) dtTrade = df_option['dtTrade'][0] # back out the spot from any forward iRate = df_option['iRate'][0] iDiv = df_option['iDiv'][0] TTM = df_option['TTM'][0] Fwd = df_option['Fwd'][0] spot = SimpleQuote(Fwd * np.exp(-(iRate - iDiv) * TTM)) print('Spot: %f risk-free rate: %f div. yield: %f' % (spot.value, iRate, iDiv)) # build array of option helpers hh = heston_helpers(spot, df_option, dtTrade, df_rates) options = hh['options'] spot = hh['spot'] risk_free_ts = dfToZeroCurve(df_rates['iRate'], dtTrade) dividend_ts = dfToZeroCurve(df_rates['iDiv'], dtTrade) # initial values for parameters if ival is None: ival = { 'v0': 0.1, 'kappa': 1.0, 'theta': 0.1, 'sigma': 0.5, 'rho': -.5 } process = HestonProcess(risk_free_ts, dividend_ts, spot, ival['v0'], ival['kappa'], ival['theta'], ival['sigma'], ival['rho']) model = HestonModel(process) engine = AnalyticHestonEngine(model, 64) for option in options: option.set_pricing_engine(engine) om = LevenbergMarquardt(1e-8, 1e-8, 1e-8) model.calibrate(options, om, EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8)) print('model calibration results:') print('v0: %f kappa: %f theta: %f sigma: %f rho: %f' % (model.v0, model.kappa, model.theta, model.sigma, model.rho)) calib_error = (1.0 / len(options)) * sum( [pow(o.calibration_error() * 100.0, 2) for o in options]) print('SSE: %f' % calib_error) # merge the fitted volatility and the input data set return merge_df(df_option, options, 'Heston')
def test_heston_hw_calibration(self): """ From Quantlib test suite """ print("Testing Heston Hull-White calibration...") ## Calibration of a hybrid Heston-Hull-White model using ## the finite difference HestonHullWhite pricing engine ## Input surface is based on a Heston-Hull-White model with ## Hull-White: a = 0.00883, \sigma = 0.00631 ## Heston : \nu = 0.12, \kappa = 2.0, ## \theta = 0.09, \sigma = 0.5, \rho=-0.75 ## Equity Short rate correlation: -0.5 dc = Actual365Fixed() calendar = TARGET() todays_date = Date(28, March, 2004) settings = Settings() settings.evaluation_date = todays_date r_ts = flat_rate(todays_date, 0.05, dc) ## assuming, that the Hull-White process is already calibrated ## on a given set of pure interest rate calibration instruments. hw_process = HullWhiteProcess(r_ts, a=0.00883, sigma=0.00631) q_ts = flat_rate(todays_date, 0.02, dc) s0 = SimpleQuote(100.0) # vol surface strikes = [50, 75, 90, 100, 110, 125, 150, 200] maturities = [1 / 12., 3 / 12., 0.5, 1.0, 2.0, 3.0, 5.0, 7.5, 10] vol = [ 0.482627,0.407617,0.366682,0.340110,0.314266,0.280241,0.252471,0.325552, 0.464811,0.393336,0.354664,0.329758,0.305668,0.273563,0.244024,0.244886, 0.441864,0.375618,0.340464,0.318249,0.297127,0.268839,0.237972,0.225553, 0.407506,0.351125,0.322571,0.305173,0.289034,0.267361,0.239315,0.213761, 0.366761,0.326166,0.306764,0.295279,0.284765,0.270592,0.250702,0.222928, 0.345671,0.314748,0.300259,0.291744,0.283971,0.273475,0.258503,0.235683, 0.324512,0.303631,0.293981,0.288338,0.283193,0.276248,0.266271,0.250506, 0.311278,0.296340,0.289481,0.285482,0.281840,0.276924,0.269856,0.258609, 0.303219,0.291534,0.286187,0.283073,0.280239,0.276414,0.270926,0.262173] start_v0 = 0.2 * 0.2 start_theta = start_v0 start_kappa = 0.5 start_sigma = 0.25 start_rho = -0.5 equityShortRateCorr = -0.5 corrConstraint = HestonHullWhiteCorrelationConstraint( equityShortRateCorr) heston_process = HestonProcess(r_ts, q_ts, s0, start_v0, start_kappa, start_theta, start_sigma, start_rho) h_model = HestonModel(heston_process) h_engine = AnalyticHestonEngine(h_model) options = [] # first calibrate a heston model to get good initial # parameters for i in range(len(maturities)): maturity = Period(int(maturities[i] * 12.0 + 0.5), Months) for j, s in enumerate(strikes): v = SimpleQuote(vol[i * len(strikes) + j]) helper = HestonModelHelper(maturity, calendar, s0.value, s, v, r_ts, q_ts, PriceError) helper.set_pricing_engine(h_engine) options.append(helper) om = LevenbergMarquardt(1e-6, 1e-8, 1e-8) # Heston model h_model.calibrate(options, om, EndCriteria(400, 40, 1.0e-8, 1.0e-4, 1.0e-8)) print("Heston calibration") print("v0: %f" % h_model.v0) print("theta: %f" % h_model.theta) print("kappa: %f" % h_model.kappa) print("sigma: %f" % h_model.sigma) print("rho: %f" % h_model.rho) h_process_2 = HestonProcess(r_ts, q_ts, s0, h_model.v0, h_model.kappa, h_model.theta, h_model.sigma, h_model.rho) hhw_model = HestonModel(h_process_2) options = [] for i in range(len(maturities)): tGrid = np.max((10.0, maturities[i] * 10.0)) hhw_engine = FdHestonHullWhiteVanillaEngine( hhw_model, hw_process, equityShortRateCorr, tGrid, 61, 13, 9, 0, True, FdmSchemeDesc.Hundsdorfer()) hhw_engine.enable_multiple_strikes_caching(strikes) maturity = Period(int(maturities[i] * 12.0 + 0.5), Months) # multiple strikes engine works best if the first option # per maturity has the average strike (because the first # option is priced first during the calibration and # the first pricing is used to calculate the prices # for all strikes # list of strikes by distance from moneyness indx = np.argsort(np.abs(np.array(strikes) - s0.value)) for j, tmp in enumerate(indx): js = indx[j] s = strikes[js] v = SimpleQuote(vol[i * len(strikes) + js]) helper = HestonModelHelper(maturity, calendar, s0.value, strikes[js], v, r_ts, q_ts, PriceError) helper.set_pricing_engine(hhw_engine) options.append(helper) vm = LevenbergMarquardt(1e-6, 1e-2, 1e-2) hhw_model.calibrate(options, vm, EndCriteria(400, 40, 1.0e-8, 1.0e-4, 1.0e-8), corrConstraint) print("Heston HW calibration with FD engine") print("v0: %f" % hhw_model.v0) print("theta: %f" % hhw_model.theta) print("kappa: %f" % hhw_model.kappa) print("sigma: %f" % hhw_model.sigma) print("rho: %f" % hhw_model.rho) relTol = 0.05 expected_v0 = 0.12 expected_kappa = 2.0 expected_theta = 0.09 expected_sigma = 0.5 expected_rho = -0.75 self.assertAlmostEqual( np.abs(hhw_model.v0 - expected_v0) / expected_v0, 0, delta=relTol) self.assertAlmostEqual( np.abs(hhw_model.theta - expected_theta) / expected_theta, 0, delta=relTol) self.assertAlmostEqual( np.abs(hhw_model.kappa - expected_kappa) / expected_kappa, 0, delta=relTol) self.assertAlmostEqual( np.abs(hhw_model.sigma - expected_sigma) / expected_sigma, 0, delta=relTol) self.assertAlmostEqual( np.abs(hhw_model.rho - expected_rho) / expected_rho, 0, delta=relTol)
# The simulation # -------------- # # The *simulate* function is not part of Quantlib. It has been added # to the pyQL interface (see folder quantlib/sim). This illustrates # how to create extensions to Quantlib and expose them to python. # <codecell> import pylab as pl from quantlib.sim.simulate import simulateHeston # simulate and plot Heston paths paths = 20 steps = 100 horizon = 2 seed = 12345 model = HestonModel(process) res = simulateHeston(model, paths, steps, horizon, seed) time = res[0, :] simulations = res[1:, :].T pl.plot(time, simulations) pl.xlabel('Time') pl.ylabel('Stock Price') pl.title('Heston Process Simulation') pl.show()
v0 = .1 kappa_v = 2 theta_v = 0.1 sigma_v = 0.3 rho_sv = -0.5 hestonProcess = HestonProcess(risk_free_rate_ts=r_ts, dividend_ts=q_ts, s0=s0, v0=v0, kappa=kappa_v, theta=theta_v, sigma=sigma_v, rho=rho_sv) hestonModel = HestonModel(hestonProcess) # Hull-White kappa_r = 1 sigma_r = .2 hullWhiteProcess = HullWhiteProcess(r_ts, a=kappa_r, sigma=sigma_r) strike = 100 maturity = 1 type = Call maturity_date = todays_date + Period(maturity, Years) exercise = EuropeanExercise(maturity_date)
def test_compare_bsm_bsmhw_hestonhw(self): dc = Actual365Fixed() todays_date = today() settings = Settings() settings.evaluation_date = todays_date tol = 1.e-2 spot = SimpleQuote(100) dates = [todays_date + Period(i, Years) for i in range(40)] rates = [0.01 + 0.0002 * np.exp(np.sin(i / 4.0)) for i in range(40)] divRates = [0.02 + 0.0001 * np.exp(np.sin(i / 5.0)) for i in range(40)] s0 = SimpleQuote(100) r_ts = ZeroCurve(dates, rates, dc) q_ts = ZeroCurve(dates, divRates, dc) vol = SimpleQuote(0.25) vol_ts = BlackConstantVol(todays_date, NullCalendar(), vol.value, dc) bsm_process = BlackScholesMertonProcess(spot, q_ts, r_ts, vol_ts) payoff = PlainVanillaPayoff(Call, 100) exercise = EuropeanExercise(dates[1]) option = VanillaOption(payoff, exercise) analytic_european_engine = AnalyticEuropeanEngine(bsm_process) option.set_pricing_engine(analytic_european_engine) npv_bsm = option.npv variance = vol.value * vol.value hestonProcess = HestonProcess(risk_free_rate_ts=r_ts, dividend_ts=q_ts, s0=s0, v0=variance, kappa=5.0, theta=variance, sigma=1e-4, rho=0.0) hestonModel = HestonModel(hestonProcess) hullWhiteModel = HullWhite(r_ts, a=0.01, sigma=0.01) bsmhwEngine = AnalyticBSMHullWhiteEngine(0.0, bsm_process, hullWhiteModel) hestonHwEngine = AnalyticHestonHullWhiteEngine(hestonModel, hullWhiteModel, 128) hestonEngine = AnalyticHestonEngine(hestonModel, 144) option.set_pricing_engine(hestonEngine) npv_heston = option.npv option.set_pricing_engine(bsmhwEngine) npv_bsmhw = option.npv option.set_pricing_engine(hestonHwEngine) npv_hestonhw = option.npv print("calculated with BSM: %f" % npv_bsm) print("BSM-HW: %f" % npv_bsmhw) print("Heston: %f" % npv_heston) print("Heston-HW: %f" % npv_hestonhw) self.assertAlmostEqual(npv_bsm, npv_bsmhw, delta=tol) self.assertAlmostEqual(npv_bsm, npv_hestonhw, delta=tol)
def test_zanette(self): """ From paper by A. Zanette et al. """ dc = Actual365Fixed() todays_date = today() settings = Settings() settings.evaluation_date = todays_date # constant yield and div curves dates = [todays_date + Period(i, Years) for i in range(3)] rates = [0.04 for i in range(3)] divRates = [0.03 for i in range(3)] r_ts = ZeroCurve(dates, rates, dc) q_ts = ZeroCurve(dates, divRates, dc) s0 = SimpleQuote(100) # Heston model v0 = .1 kappa_v = 2 theta_v = 0.1 sigma_v = 0.3 rho_sv = -0.5 hestonProcess = HestonProcess(risk_free_rate_ts=r_ts, dividend_ts=q_ts, s0=s0, v0=v0, kappa=kappa_v, theta=theta_v, sigma=sigma_v, rho=rho_sv) hestonModel = HestonModel(hestonProcess) # Hull-White kappa_r = 1 sigma_r = .2 hullWhiteProcess = HullWhiteProcess(r_ts, a=kappa_r, sigma=sigma_r) strike = 100 maturity = 1 type = Call maturity_date = todays_date + Period(maturity, Years) exercise = EuropeanExercise(maturity_date) payoff = PlainVanillaPayoff(type, strike) option = VanillaOption(payoff, exercise) def price_cal(rho, tGrid): fd_hestonHwEngine = FdHestonHullWhiteVanillaEngine( hestonModel, hullWhiteProcess, rho, tGrid, 100, 40, 20, 0, True, FdmSchemeDesc.Hundsdorfer()) option.set_pricing_engine(fd_hestonHwEngine) return option.npv calc_price = [] for rho in [-0.5, 0, .5]: for tGrid in [50, 100, 150, 200]: tmp = price_cal(rho, tGrid) print("rho (S,r): %f Ns: %d Price: %f" % (rho, tGrid, tmp)) calc_price.append(tmp) expected_price = [ 11.38, ] * 4 + [ 12.79, ] * 4 + [ 14.06, ] * 4 np.testing.assert_almost_equal(calc_price, expected_price, 2)
def test_compare_BsmHW_HestonHW(self): """ From Quantlib test suite """ print("Comparing European option pricing for a BSM " + "process with one-factor Hull-White model...") dc = Actual365Fixed() todays_date = today() settings = Settings() settings.evaluation_date = todays_date tol = 1.e-2 spot = SimpleQuote(100) dates = [todays_date + Period(i, Years) for i in range(40)] rates = [0.01 + 0.0002 * np.exp(np.sin(i / 4.0)) for i in range(40)] divRates = [0.02 + 0.0001 * np.exp(np.sin(i / 5.0)) for i in range(40)] s0 = SimpleQuote(100) r_ts = ZeroCurve(dates, rates, dc) q_ts = ZeroCurve(dates, divRates, dc) vol = SimpleQuote(0.25) vol_ts = BlackConstantVol(todays_date, NullCalendar(), vol.value, dc) bsm_process = BlackScholesMertonProcess(spot, q_ts, r_ts, vol_ts) variance = vol.value * vol.value hestonProcess = HestonProcess(risk_free_rate_ts=r_ts, dividend_ts=q_ts, s0=s0, v0=variance, kappa=5.0, theta=variance, sigma=1e-4, rho=0.0) hestonModel = HestonModel(hestonProcess) hullWhiteModel = HullWhite(r_ts, a=0.01, sigma=0.01) bsmhwEngine = AnalyticBSMHullWhiteEngine(0.0, bsm_process, hullWhiteModel) hestonHwEngine = AnalyticHestonHullWhiteEngine(hestonModel, hullWhiteModel, 128) tol = 1e-5 strikes = [0.25, 0.5, 0.75, 0.8, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0, 4.0] maturities = [1, 2, 3, 5, 10, 15, 20, 25, 30] types = [Put, Call] for type in types: for strike in strikes: for maturity in maturities: maturity_date = todays_date + Period(maturity, Years) exercise = EuropeanExercise(maturity_date) fwd = strike * s0.value * \ q_ts.discount(maturity_date) / \ r_ts.discount(maturity_date) payoff = PlainVanillaPayoff(type, fwd) option = VanillaOption(payoff, exercise) option.set_pricing_engine(bsmhwEngine) calculated = option.npv option.set_pricing_engine(hestonHwEngine) expected = option.npv if ((np.abs(expected - calculated) > calculated * tol) and (np.abs(expected - calculated) > tol)): cp = PAYOFF_TO_STR[type] print("Failed to reproduce npv") print("strike : %f" % strike) print("maturity : %d" % maturity) print("type : %s" % cp) self.assertAlmostEqual(expected, calculated, delta=tol)
def test_black_calibration(self): # calibrate a Heston model to a constant volatility surface without # smile. expected result is a vanishing volatility of the volatility. # In addition theta and v0 should be equal to the constant variance todays_date = today() self.settings.evaluation_date = todays_date daycounter = Actual360() calendar = NullCalendar() risk_free_ts = flat_rate(0.04, daycounter) dividend_ts = flat_rate(0.50, daycounter) option_maturities = [ Period(1, Months), Period(2, Months), Period(3, Months), Period(6, Months), Period(9, Months), Period(1, Years), Period(2, Years) ] options = [] s0 = SimpleQuote(1.0) vol = SimpleQuote(0.1) volatility = vol.value for maturity in option_maturities: for moneyness in np.arange(-1.0, 2.0, 1.): tau = daycounter.year_fraction( risk_free_ts.reference_date, calendar.advance( risk_free_ts.reference_date, period=maturity) ) forward_price = s0.value * dividend_ts.discount(tau) / \ risk_free_ts.discount(tau) strike_price = forward_price * np.exp( -moneyness * volatility * np.sqrt(tau) ) options.append( HestonModelHelper( maturity, calendar, s0.value, strike_price, vol, risk_free_ts, dividend_ts ) ) for sigma in np.arange(0.1, 0.7, 0.2): v0 = 0.01 kappa = 0.2 theta = 0.02 rho = -0.75 process = HestonProcess( risk_free_ts, dividend_ts, s0, v0, kappa, theta, sigma, rho ) self.assertEqual(v0, process.v0) self.assertEqual(kappa, process.kappa) self.assertEqual(theta, process.theta) self.assertEqual(sigma, process.sigma) self.assertEqual(rho, process.rho) self.assertEqual(1.0, process.s0.value) model = HestonModel(process) engine = AnalyticHestonEngine(model, 96) for option in options: option.set_pricing_engine(engine) optimisation_method = LevenbergMarquardt(1e-8, 1e-8, 1e-8) end_criteria = EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8) model.calibrate(options, optimisation_method, end_criteria) tolerance = 3.0e-3 self.assertFalse(model.sigma > tolerance) self.assertAlmostEqual( model.kappa * model.theta, model.kappa * volatility ** 2, delta=tolerance ) self.assertAlmostEqual(model.v0, volatility ** 2, delta=tolerance)
def heston_calibration(df_option, ival=None): """ calibrate heston model """ # extract rates and div yields from the data set df_tmp = DataFrame.filter(df_option, items=['dtExpiry', 'iRate', 'iDiv']) grouped = df_tmp.groupby('dtExpiry') def aggregate(serie): return serie[serie.index[0]] df_rates = grouped.agg(aggregate) # Get first index: first_index = 0 dtTrade = df_option['dtTrade'][first_index] # back out the spot from any forward iRate = df_option['iRate'][first_index] iDiv = df_option['iDiv'][first_index] TTM = df_option['TTM'][first_index] Fwd = df_option['Fwd'][first_index] spot = SimpleQuote(Fwd*np.exp(-(iRate-iDiv)*TTM)) print('Spot: %f risk-free rate: %f div. yield: %f' % (spot.value, iRate, iDiv)) # build array of option helpers hh = heston_helpers(spot, df_option, dtTrade, df_rates) options = hh['options'] spot = hh['spot'] risk_free_ts = dfToZeroCurve(df_rates['iRate'], dtTrade) dividend_ts = dfToZeroCurve(df_rates['iDiv'], dtTrade) # initial values for parameters if ival is None: ival = {'v0': 0.1, 'kappa': 1.0, 'theta': 0.1, 'sigma': 0.5, 'rho': -.5} process = HestonProcess( risk_free_ts, dividend_ts, spot, ival['v0'], ival['kappa'], ival['theta'], ival['sigma'], ival['rho']) model = HestonModel(process) engine = AnalyticHestonEngine(model, 64) for option in options: option.set_pricing_engine(engine) om = LevenbergMarquardt(1e-8, 1e-8, 1e-8) model.calibrate( options, om, EndCriteria(400, 40, 1.0e-8, 1.0e-8, 1.0e-8) ) print('model calibration results:') print('v0: %f kappa: %f theta: %f sigma: %f rho: %f' % (model.v0, model.kappa, model.theta, model.sigma, model.rho)) calib_error = (1.0/len(options)) * sum( [pow(o.calibration_error()*100.0,2) for o in options]) print('SSE: %f' % calib_error) # merge the fitted volatility and the input data set return merge_df(df_option, options, 'Heston')