def loss_function(x): """Loss function for the optimization.""" x_mr = _to_constrained(x[:num_mean_reversion], mr_lb, mr_ub) x_vol = _to_constrained(x[num_mean_reversion:], vol_lb, vol_ub) mean_reversion_param = piecewise.PiecewiseConstantFunc( jump_locations=[], values=x_mr, dtype=dtype) volatility_param = piecewise.PiecewiseConstantFunc( jump_locations=volatility.jump_locations(), values=x_vol, dtype=dtype) model_values = cap_floor.cap_floor_price( strikes=strikes, expiries=expiries, maturities=maturities, daycount_fractions=daycount_fractions, reference_rate_fn=reference_rate_fn, dim=dim, mean_reversion=mean_reversion_param, volatility=volatility_param, notional=notional, is_cap=is_cap, use_analytic_pricing=use_analytic_pricing, num_samples=num_samples, random_type=random_type, seed=seed, skip=skip, time_step=time_step, dtype=dtype)[:, 0] return tf.math.reduce_mean( (_scale(model_values, target_lb, target_ub) - scaled_target)**2)
def loss_function(x): """Loss function for the optimization.""" x_mr = _to_constrained(x[:num_mean_reversion], mr_lb, mr_ub) x_vol = _to_constrained(x[num_mean_reversion:], vol_lb, vol_ub) mean_reversion_param = piecewise.PiecewiseConstantFunc( jump_locations=[], values=x_mr, dtype=dtype) volatility_param = piecewise.PiecewiseConstantFunc( jump_locations=volatility.jump_locations(), values=x_vol, dtype=dtype) model_values = swaption.swaption_price( expiries=expiries, floating_leg_start_times=float_leg_start_times, floating_leg_end_times=float_leg_end_times, fixed_leg_payment_times=fixed_leg_payment_times, floating_leg_daycount_fractions=floating_leg_daycount_fractions, fixed_leg_daycount_fractions=fixed_leg_daycount_fractions, fixed_leg_coupon=fixed_leg_coupon, reference_rate_fn=reference_rate_fn, dim=1, mean_reversion=mean_reversion_param, volatility=volatility_param, notional=notional, is_payer_swaption=is_payer_swaption, use_analytic_pricing=use_analytic_pricing, num_samples=num_samples, random_type=random_type, seed=seed, skip=skip, time_step=time_step, dtype=dtype)[:, 0] if volatility_based_calibration: model_values = implied_vol( prices=model_values / annuity / notional, strikes=fixed_leg_coupon[..., 0], expiries=expiries, forwards=swap_rate, is_call_options=is_payer_swaption, underlying_distribution=UnderlyingDistribution.NORMAL, dtype=dtype) model_values = tf.where(tf.math.is_nan(model_values), tf.zeros_like(model_values), model_values) print(x_mr, x_vol, model_values) value = tf.math.reduce_sum( (_scale(model_values, target_lb, target_ub) - scaled_target)**2) return value
def _volatility_squared_from_volatility(self, volatility, volatility_is_constant, dtype=None, name=None): """Returns volatility squared as either a `PiecewiseConstantFunc` or a `Tensor`. Args: volatility: Either a 'Tensor' or 'PiecewiseConstantFunc'. volatility_is_constant: `bool` which is True if volatility is of type `Tensor`. dtype: The default dtype to use when converting values to `Tensor`s. Default value: `None` which means that default dtypes inferred by TensorFlow are used. name: Python string. The name to give to the ops created by this class. Default value: `None` which maps to the default name '_volatility_squared'. """ name = name or (self._name + '_volatility_squared') if volatility_is_constant: return volatility**2 else: return pw.PiecewiseConstantFunc(volatility.jump_locations(), volatility.values()**2, dtype=dtype, name=name)
def test_invalid_value_event_shape(self): """Tests that `values` event shape is `jump_locations` event shape + 1.""" for dtype in [np.float32, np.float64]: jump_locations = np.array([[0.1, 10], [2., 10]]) values = tf.constant([[3, 4, 5, 6], [3, 4, 5, 7]], dtype=dtype) with self.assertRaises(ValueError): piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype)
def test_invalid_jump_batch_shape(self): """Tests that `jump_locations` and `values` should have the same batch.""" for dtype in [np.float32, np.float64]: jump_locations = np.array([[0.1, 10], [2., 10]]) values = tf.constant([[[3, 4, 5], [3, 4, 5]]], dtype=dtype) with self.assertRaises(ValueError): piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype)
def fn(x, jump_locations, values): piecewise_func = piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype) value = piecewise_func(x) integral = piecewise_func.integrate(x, x + 1) return value, integral
def test_convert_to_tensor_or_func_PiecewiseConstantFunc(self): """Tests that tensor_or_func recognizes inputs of PiecewiseConstantFunc.""" dtype = tf.float64 times = np.arange(0, 10, 1) values = np.ones(11) pwc = piecewise.PiecewiseConstantFunc(times, values, dtype=dtype) output = piecewise.convert_to_tensor_or_func(pwc) expected = (pwc, False) self.assertAllEqual(output, expected)
def test_invalid_x_batch_shape(self): """Tests that `x` should have the same batch shape as `jump_locations`.""" for dtype in [np.float32, np.float64]: x = np.array([0., 0.1, 2., 11.]) jump_locations = np.array([[0.1, 10], [2., 10]]) values = tf.constant([[3, 4, 5], [3, 4, 5]], dtype=dtype) piecewise_func = piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype) with self.assertRaises(ValueError): piecewise_func(x, left_continuous=False)
def test_piecewise_constant_integral_no_batch(self): """Tests PiecewiseConstantFunc with no batching.""" for dtype in [np.float32, np.float64]: x = np.array([-4.1, 0., 1., 1.5, 2., 4.5, 5.5]) jump_locations = np.array([1, 2, 3, 4, 5], dtype=dtype) values = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]) piecewise_func = piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype) value = piecewise_func.integrate(x, x + 4.1) self.assertEqual(value.dtype.as_numpy_dtype, dtype) expected_value = np.array([0.41, 1.05, 1.46, 1.66, 1.86, 2.41, 2.46]) self.assertAllClose(value, expected_value, atol=1e-5, rtol=1e-5)
def test_piecewise_constant_value_no_batch(self): """Tests PiecewiseConstantFunc with no batching.""" for dtype in [np.float32, np.float64]: x = np.array([0., 0.1, 2., 11.]) jump_locations = np.array([0.1, 10], dtype=dtype) values = tf.constant([3, 4, 5], dtype=dtype) piecewise_func = piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype) # Also verifies left-continuity value = piecewise_func(x) self.assertEqual(value.dtype.as_numpy_dtype, dtype) expected_value = np.array([3., 3., 4., 5.]) self.assertAllEqual(value, expected_value)
def loss_function(x): """Loss function for the optimization.""" x_mr = _to_constrained(x[:num_mean_reversion], mr_lb, mr_ub) x_vol = _to_constrained(x[num_mean_reversion:], vol_lb, vol_ub) mean_reversion_param = piecewise.PiecewiseConstantFunc( jump_locations=[], values=x_mr, dtype=dtype) volatility_param = piecewise.PiecewiseConstantFunc( jump_locations=volatility.jump_locations(), values=x_vol, dtype=dtype) model_prices = swaption.swaption_price( expiries=expiries, floating_leg_start_times=float_leg_start_times, floating_leg_end_times=float_leg_end_times, fixed_leg_payment_times=fixed_leg_payment_times, floating_leg_daycount_fractions=floating_leg_daycount_fractions, fixed_leg_daycount_fractions=fixed_leg_daycount_fractions, fixed_leg_coupon=fixed_leg_coupon, reference_rate_fn=reference_rate_fn, dim=1, mean_reversion=mean_reversion_param, volatility=volatility_param, notional=notional, is_payer_swaption=is_payer_swaption, use_analytic_pricing=use_analytic_pricing, num_samples=num_samples, random_type=random_type, seed=seed, skip=skip, time_step=time_step, dtype=dtype)[:, 0] value = tf.math.reduce_sum( (_scale(model_prices, price_lb, price_ub) - scaled_prices)**2) return value
def _input_type(param, dim, dtype, name): """Checks if the input parameter is a callable or piecewise constant.""" # If the parameter is callable but not a piecewise constant use # generic sampling method (e.g., Euler). sample_with_generic = False if hasattr(param, 'is_piecewise_constant'): if param.is_piecewise_constant: jumps_shape = param.jump_locations().shape if len(jumps_shape) > 2: raise ValueError( 'Batch rank of `jump_locations` should be `1` for all piecewise ' 'constant arguments but {} instead'.format( len(jumps_shape[:-1]))) if len(jumps_shape) == 2: if dim != jumps_shape[0]: raise ValueError( 'Batch shape of `jump_locations` should be either empty or ' '`[{0}]` but `[{1}]` instead'.format( dim, jumps_shape[0])) if name == 'mean_reversion' and jumps_shape[0] > 0: # Exact discretization currently not supported with time-dependent mr sample_with_generic = True return param, sample_with_generic else: sample_with_generic = True elif callable(param): sample_with_generic = True else: # Otherwise, input is a `Tensor`, return a `PiecewiseConstantFunc`. param = tf.convert_to_tensor(param, dtype=dtype, name=name) param_shape = param.shape.as_list() param_rank = param.shape.rank if param_shape[-1] != dim: # This is an error, we need as many parameters as the number of `dim` raise ValueError( 'Length of {} ({}) should be the same as `dims`({}).'.format( name, param_shape[0], dim)) if param_rank == 2: # This is when the parameter is a correlation matrix jump_locations = [] values = tf.expand_dims(param, axis=0) else: jump_locations = [] if dim == 1 else [[]] * dim values = param if dim == 1 else tf.expand_dims(param, axis=-1) param = piecewise.PiecewiseConstantFunc(jump_locations=jump_locations, values=values, dtype=dtype) return param, sample_with_generic
def test_different_x1_x2_batch_shape(self): """Tests that `x1` and `x2` should have the same batch shape.""" for dtype in [np.float32, np.float64]: x1 = np.array([[0., 0.1, 2., 11.]]) x2 = np.array([[0., 0.1, 2., 11.], [0., 0.1, 2., 11.]]) x3 = x2 + 1 jump_locations = np.array([[0.1, 10]]) values = tf.constant([[3, 4, 5]], dtype=dtype) piecewise_func = piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype) with self.assertRaises(ValueError): piecewise_func.integrate(x1, x2) # `x2` and `x3` have the same batch shape but different from # the batch shape of `jump_locations` with self.assertRaises(ValueError): piecewise_func.integrate(x2, x3)
def _sigma_squared_from_sigma(self, sigma, sigma_is_constant, dtype=None, name=None): """Returns sigma squared as either a `PiecewiseConstantFunc` or a `Tensor`. Args: sigma: Either a 'Tensor' or 'PiecewiseConstantFunc'. sigma_is_constant: `bool` which is True if sigma is of type `Tensor`. dtype: The default dtype to use when converting values to `Tensor`s. Default value: `None` which means that default dtypes inferred by TensorFlow are used. name: Python string. The name to give to the ops created by this class. Default value: `None` which maps to the default name '_sigma_squared'. """ name = name or (self._name + "_sigma_squared") return sigma ** 2 if sigma_is_constant else piecewise.PiecewiseConstantFunc( sigma.jump_locations(), sigma.values()**2, dtype=dtype, name=name)
def test_piecewise_constant_value_with_batch_and_repetitions(self): """Tests PiecewiseConstantFunc with batching and repetitive values.""" for dtype in [np.float32, np.float64]: x = tf.constant([[-4.1, 0.1, 1., 2., 10, 11.], [1., 2., 3., 2., 5., 9.]], dtype=dtype) jump_locations = tf.constant([[0.1, 0.1, 1., 1., 10., 10.], [-1., 1.2, 2.2, 2.2, 2.2, 8.]], dtype=dtype) values = tf.constant([[3, 3, 4, 5, 5., 2, 6.], [-1, -5, 2, 5, 5., 5., 1.]], dtype=dtype) piecewise_func = piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype) # Also verifies left-continuity value = piecewise_func(x, left_continuous=True) self.assertEqual(value.dtype.as_numpy_dtype, dtype) expected_value = np.array([[3., 3., 4., 5., 5., 6.], [-5., 2., 5., 2., 5., 1.]]) self.assertAllEqual(value, expected_value)
def test_piecewise_constant_integral_with_batch(self): """Tests PiecewiseConstantFunc with batching.""" for dtype in [np.float32, np.float64]: x = np.array([[[0.0, 0.1, 2.0, 11.0], [0.0, 2.0, 3.0, 9.0]], [[0.0, 1.0, 2.0, 3.0], [4.0, 5.0, 6.0, 7.0]]]) jump_locations = np.array([[[0.1, 10.0], [1.5, 10.0]], [[1.0, 2.0], [5.0, 6.0]]]) values = tf.constant([[[3, 4, 5], [3, 4, 5]], [[3, 4, 5], [3, 4, 5]]], dtype=dtype) piecewise_func = piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype) value = piecewise_func.integrate(x, x + 1.1) self.assertEqual(value.dtype.as_numpy_dtype, dtype) expected_value = np.array([[[4.3, 4.4, 4.4, 5.5], [3.3, 4.4, 4.4, 4.5]], [[3.4, 4.5, 5.5, 5.5], [3.4, 4.5, 5.5, 5.5]]]) self.assertAllClose(value, expected_value, atol=1e-5, rtol=1e-5)
def test_piecewise_constant_value_with_batch(self): """Tests PiecewiseConstantFunc with batching.""" for dtype in [np.float32, np.float64]: x = np.array([[[0.0, 0.1, 2.0, 11.0], [0.0, 2.0, 3.0, 9.0]], [[0.0, 1.0, 2.0, 3.0], [4.0, 5.0, 6.0, 7.0]]]) jump_locations = np.array([[[0.1, 10.0], [1.5, 10.0]], [[1.0, 2.0], [5.0, 6.0]]]) values = tf.constant([[[3, 4, 5], [3, 4, 5]], [[3, 4, 5], [3, 4, 5]]], dtype=dtype) piecewise_func = piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype) # Also verifies right-continuity value = piecewise_func(x, left_continuous=False) self.assertEqual(value.dtype.as_numpy_dtype, dtype) expected_value = np.array([[[3.0, 4.0, 4.0, 5.0], [3.0, 4.0, 4.0, 4.0]], [[3.0, 4.0, 5.0, 5.0], [3.0, 4.0, 5.0, 5.0]]]) self.assertAllEqual(value, expected_value)
def test_matrix_event_shape_no_batch_shape(self): """Tests that `values` event shape is `jump_locations` event shape + 1.""" for dtype in [np.float32, np.float64]: x = np.array([0., 0.1, 2., 11.]) jump_locations = [0.1, 10] values = [[[1, 2], [3, 4]], [[5, 6], [7, 8]], [[9, 10], [11, 12]]] piecewise_func = piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype) value = piecewise_func(x) integral = piecewise_func.integrate(x, x + 1) expected_value = [[[1, 2], [3, 4]], [[1, 2], [3, 4]], [[5, 6], [7, 8]], [[9, 10], [11, 12]]] expected_integral = [[[4.6, 5.6], [6.6, 7.6]], [[5, 6], [7, 8]], [[5, 6], [7, 8]], [[9, 10], [11, 12]]] self.assertAllClose(value, expected_value, atol=1e-5, rtol=1e-5) self.assertAllClose(integral, expected_integral, atol=1e-5, rtol=1e-5)
def test_3d_event_shape_with_batch_shape(self): """Tests that `values` event shape is `jump_locations` event shape + 1.""" for dtype in [np.float32, np.float64]: x = np.array([[0, 1, 2, 3], [0.5, 1.5, 2.5, 3.5]]) jump_locations = [[0.5, 2], [0.5, 1.5]] values = [[[0, 1, 1.5], [2, 3, 0], [1, 0, 1]], [[0, 0.5, 1], [1, 3, 2], [2, 3, 1]]] piecewise_func = piecewise.PiecewiseConstantFunc(jump_locations, values, dtype=dtype) value = piecewise_func(x) integral = piecewise_func.integrate(x, x + 1) expected_value = [[[0, 1, 1.5], [2, 3, 0], [2, 3, 0], [1, 0, 1]], [[0, 0.5, 1], [1, 3, 2], [2, 3, 1], [2, 3, 1]]] expected_integral = [[[1, 2, 0.75], [2, 3, 0], [1, 0, 1], [1, 0, 1]], [[1, 3, 2], [2, 3, 1], [2, 3, 1], [2, 3, 1]]] self.assertAllClose(value, expected_value, atol=1e-5, rtol=1e-5) self.assertAllClose(integral, expected_integral, atol=1e-5, rtol=1e-5)
def calibration_from_cap_floors( *, prices, strikes, expiries, maturities, daycount_fractions, reference_rate_fn, mean_reversion, volatility, notional=1.0, # TODO(b/183418183) Allow for dim > 1 dim=1, is_cap=True, use_analytic_pricing=True, num_samples=1, random_type=None, seed=None, skip=0, time_step=None, optimizer_fn=None, mean_reversion_lower_bound=0.001, mean_reversion_upper_bound=0.5, volatility_lower_bound=0.00001, volatility_upper_bound=0.1, tolerance=1e-6, maximum_iterations=50, dtype=None, name=None): """Calibrates the Hull-White model using the observed Cap/Floor prices. This function estimates the mean-reversion rate and volatility parameters of a Hull-White 1-factor model using a set of Cap/Floor prices as the target. The calibration is performed using least-squares optimization where the loss function minimizes the squared error between the observed option prices and the model implied prices. #### Example The example shows how to calibrate a Hull-White model with constant mean reversion rate and constant volatility. ````python import numpy as np import tensorflow.compat.v2 as tf import tf_quant_finance as tff # In this example, we synthetically generate some prices. Then we use our # calibration to back out these prices. dtype = tf.float64 daycount_fractions = np.array([ [0.25, 0.25, 0.25, 0.25, 0.0, 0.0, 0.0, 0.0], [0.25, 0.25, 0.25, 0.25, 0.0, 0.0, 0.0, 0.0], [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], ]) expiries = np.array([ [0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.0, 0.0], [0.0, 0.25, 0.5, 0.75, 1.0, 0.0, 0.0, 0.0], [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75], [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75], ]) maturities = np.array([ [0.25, 0.5, 0.75, 1.0, 0.0, 0.0, 0.0, 0.0], [0.25, 0.5, 0.75, 1.0, 0.0, 0.0, 0.0, 0.0], [0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0], [0.25, 0.5, 0.75, 1.0, 1.25, 1.50, 1.75, 2.0], ]) is_cap = np.array([True, False, True, False]) strikes = 0.01 * np.ones_like(expiries) # Setup - generate some observed prices using the model. expected_mr = [0.4] expected_vol = [0.01] zero_rate_fn = lambda x: 0.01 * tf.ones_like(x, dtype=dtype) prices = tff.models.hull_white.cap_floor_price( strikes=strikes, expiries=expiries, maturities=maturities, daycount_fractions=daycount_fractions, reference_rate_fn=zero_rate_fn, notional=1.0, dim=1, mean_reversion=expected_mr, volatility=expected_vol, is_cap=tf.expand_dims(is_cap, axis=1), use_analytic_pricing=True, dtype=dtype) # Calibrate the model. calibrated_model, is_converged, _ = ( tff.models.hull_white.calibration_from_cap_floors( prices=tf.squeeze(prices), strikes=strikes, expiries=expiries, maturities=maturities, daycount_fractions=daycount_fractions, reference_rate_fn=zero_rate_fn, mean_reversion=[0.3], volatility=[0.02], notional=1.0, dim=1, is_cap=tf.expand_dims(is_cap, axis=1), use_analytic_pricing=True, optimizer_fn=None, num_samples=1000, random_type=tff.math.random.RandomType.STATELESS_ANTITHETIC, seed=[0, 0], time_step=0.1, maximum_iterations=200, dtype=dtype)) calibrated_mr = calibrated_model.mean_reversion.values() calibrated_vol = calibrated_model.volatility.values() # Running this inside a unit test passes: # # calibrated_mr, calibrated_vol = self.evaluate( # [calibrated_mr, calibrated_vol]) # self.assertTrue(is_converged) # self.assertAllClose(calibrated_mr, expected_mr, atol=1e-3, rtol=1e-2) # self.assertAllClose(calibrated_vol, expected_vol, atol=1e-3, rtol=1e-2) ```` Args: prices: A real `Tensor` of shape [num_capfloors], holding the prices of cap/floors used for calibration; e.g. `prices[i]` holds the price for the i-th cap/floor. strikes: A real `Tensor` of shape [num_capfloors, num_optionlets], where the second axis corresponds to the strikes of the caplets or floorlets contained within each option; e.g. `strikes[i, j]` holds the strike price for the j-th caplet/floorlet of the i-th cap/floor. expiries: A real `Tensor` of shape [num_capfloors, num_optionlets], where `expiries[i, j]` holds the reset time for the j-th caplet/floorlet of the i-th cap/floor. maturities: A real `Tensor` of shape [num_capfloors, num_optionlets], where `maturities[i, j]` holds the maturity time (aka the end of accrual) of the underlying forward rate for the j-th caplet/floorlet of the i-th cap/floor. The payment occurs on the maturity as well. daycount_fractions: A real `Tensor` of shape [num_capfloors, num_optionlets], where `daycount_fractions[i, j]` holds the daycount fractions associated with the underlying forward rate of the j-th caplet/floorlet of the i-th cap/floor. reference_rate_fn: A Python callable that accepts expiry time as a real `Tensor` and returns a `Tensor` of the same shape and dtype, representing the continuously compounded zero rate at the present time for the input expiry time. mean_reversion: A real positive `Tensor` of shape broadcastable to `[dim]`, or a Python callable. The callable can be one of the following: (a) A left-continuous piecewise constant object (e.g., `tff.math.piecewise.PiecewiseConstantFunc`) that has a property `is_piecewise_constant` set to `True`. In this case the object should have a method `jump_locations(self)` that returns a `Tensor` of shape `[dim, num_jumps]` or `[num_jumps]`. In the first case, `mean_reversion(t)` should return a `Tensor` of shape `[dim] + t.shape`, and in the second, `t.shape + [dim]`, where `t` is a rank 1 `Tensor` of the same `dtype` as the output. See example in the class docstring. (b) A callable that accepts scalars (stands for time `t`) and returns a `Tensor` of shape `[dim]`. Corresponds to the *initial estimate* of the mean reversion rate for the calibration. volatility: A real positive `Tensor` of the same `dtype` as `mean_reversion` or a callable with the same specs as above. Corresponds to the *initial estimate* of the volatility for the calibration. notional: A real `Tensor` broadcast to [num_capfloors], such that `notional[i]` is the notional amount for the i-th cap/floor. dim: A Python scalar which corresponds to the number of Hull-White Models to be used for pricing. Default value: The default value is `1`. Currently, dim > 1 is not yet implemented. is_cap: A boolean tensor broadcastable to [num_capfloors], such that `is_cap[i]` represents whether or not the i-th instrument is a cap (True) or floor (False). use_analytic_pricing: A Python boolean specifying if cap/floor pricing is done analytically during calibration. Analytic valuation is only supported for constant `mean_reversion` and piecewise constant `volatility`. If the input is `False`, then valuation using Monte-Carlo simulations is performed. Default value: The default value is `True`. num_samples: Positive scalar `int32` `Tensor`. The number of simulation paths during Monte-Carlo valuation of cap/floors. This input is ignored during analytic valuation. Default value: The default value is 1. random_type: Enum value of `RandomType`. The type of (quasi)-random number generator to use to generate the simulation paths. This input is relevant only for Monte-Carlo valuation and ignored during analytic valuation. Default value: `None` which maps to the standard pseudo-random numbers. seed: Seed for the random number generator. The seed is only relevant if `random_type` is one of `[STATELESS, PSEUDO, HALTON_RANDOMIZED, PSEUDO_ANTITHETIC, STATELESS_ANTITHETIC]`. For `PSEUDO`, `PSEUDO_ANTITHETIC` and `HALTON_RANDOMIZED` the seed should be an Python integer. For `STATELESS` and `STATELESS_ANTITHETIC `must be supplied as an integer `Tensor` of shape `[2]`. This input is relevant only for Monte-Carlo valuation and ignored during analytic valuation. Default value: `None` which means no seed is set. skip: `int32` 0-d `Tensor`. The number of initial points of the Sobol or Halton sequence to skip. Used only when `random_type` is 'SOBOL', 'HALTON', or 'HALTON_RANDOMIZED', otherwise ignored. Default value: `0`. time_step: Scalar real `Tensor`. Maximal distance between time grid points in Euler scheme. Relevant when Euler scheme is used for simulation. This input is ignored during analytic valuation. Default value: `None`. optimizer_fn: Optional Python callable which implements the algorithm used to minimize the objective function during calibration. It should have the following interface: result = optimizer_fn(value_and_gradients_function, initial_position, tolerance, max_iterations) `value_and_gradients_function` is a Python callable that accepts a point as a real `Tensor` and returns a tuple of `Tensor`s of real dtype containing the value of the function and its gradient at that point. 'initial_position' is a real `Tensor` containing the starting point of the optimization, 'tolerance' is a real scalar `Tensor` for stopping tolerance for the procedure and `max_iterations` specifies the maximum number of iterations. `optimizer_fn` should return a namedtuple containing the items: `position` (a tensor containing the optimal value), `converged` (a boolean indicating whether the optimize converged according the specified criteria), `failed` (a boolean indicating if the optimization resulted in a failure), `num_iterations` (the number of iterations used), and `objective_value` ( the value of the objective function at the optimal value). The default value for `optimizer_fn` is None and conjugate gradient algorithm is used. mean_reversion_lower_bound: An optional scalar `Tensor` specifying the lower limit of mean reversion rate during calibration. Default value: 0.001. mean_reversion_upper_bound: An optional scalar `Tensor` specifying the upper limit of mean reversion rate during calibration. Default value: 0.5. volatility_lower_bound: An optional scalar `Tensor` specifying the lower limit of Hull White volatility during calibration. Default value: 0.00001 (0.1 basis points). volatility_upper_bound: An optional scalar `Tensor` specifying the upper limit of Hull White volatility during calibration. Default value: 0.1. tolerance: Scalar `Tensor` of real dtype. The absolute tolerance for terminating the iterations. Default value: 1e-6. maximum_iterations: Scalar positive int32 `Tensor`. The maximum number of iterations during the optimization. Default value: 50. dtype: The default dtype to use when converting values to `Tensor`s. Default value: `None` which means that default dtypes inferred from `prices` is used. name: Python string. The name to give to the ops created by this function. Default value: `None` which maps to the default name `hw_capfloor_calibration`. Returns: A Tuple of three elements. The first element is an instance of `HullWhite1F` whose parameters are calibrated to the input swaption prices. The second and third elements contains the optimization status (whether the optimization algorithm succeeded in finding the optimal point based on the specified convergance criteria) and the number of iterations performed. """ name = name or 'hw_capfloor_calibration' with tf.name_scope(name): prices = tf.convert_to_tensor(prices, dtype=dtype, name='prices') dtype = dtype or prices.dtype strikes = tf.convert_to_tensor(strikes, dtype=dtype, name='strikes') expiries = tf.convert_to_tensor(expiries, dtype=dtype, name='expiries') maturities = tf.convert_to_tensor(maturities, dtype=dtype, name='maturities') daycount_fractions = tf.convert_to_tensor(daycount_fractions, dtype=dtype, name='daycount_fractions') notional = tf.convert_to_tensor(notional, dtype=dtype, name='notional') is_cap = tf.convert_to_tensor(is_cap, name='is_cap', dtype=tf.bool) if not hasattr(mean_reversion, 'is_piecewise_constant'): mean_reversion = piecewise.PiecewiseConstantFunc( jump_locations=[], values=mean_reversion, dtype=dtype) if not hasattr(volatility, 'is_piecewise_constant'): volatility = piecewise.PiecewiseConstantFunc(jump_locations=[], values=volatility, dtype=dtype) if optimizer_fn is None: optimizer_fn = optimizer.conjugate_gradient_minimize target_values = prices target_lb = tf.constant(0.0, dtype=dtype) target_ub = tf.math.reduce_max(target_values) vol_lb = tf.convert_to_tensor(volatility_lower_bound, dtype=dtype) vol_ub = tf.convert_to_tensor(volatility_upper_bound, dtype=dtype) mr_lb = tf.convert_to_tensor(mean_reversion_lower_bound, dtype=dtype) mr_ub = tf.convert_to_tensor(mean_reversion_upper_bound, dtype=dtype) initial_guess = tf.concat([ _to_unconstrained(mean_reversion.values(), mr_lb, mr_ub), _to_unconstrained(volatility.values(), vol_lb, vol_ub) ], axis=0) num_mean_reversion = mean_reversion.values().shape.as_list()[0] scaled_target = _scale(target_values, target_lb, target_ub) @make_val_and_grad_fn def loss_function(x): """Loss function for the optimization.""" x_mr = _to_constrained(x[:num_mean_reversion], mr_lb, mr_ub) x_vol = _to_constrained(x[num_mean_reversion:], vol_lb, vol_ub) mean_reversion_param = piecewise.PiecewiseConstantFunc( jump_locations=[], values=x_mr, dtype=dtype) volatility_param = piecewise.PiecewiseConstantFunc( jump_locations=volatility.jump_locations(), values=x_vol, dtype=dtype) model_values = cap_floor.cap_floor_price( strikes=strikes, expiries=expiries, maturities=maturities, daycount_fractions=daycount_fractions, reference_rate_fn=reference_rate_fn, dim=dim, mean_reversion=mean_reversion_param, volatility=volatility_param, notional=notional, is_cap=is_cap, use_analytic_pricing=use_analytic_pricing, num_samples=num_samples, random_type=random_type, seed=seed, skip=skip, time_step=time_step, dtype=dtype)[:, 0] return tf.math.reduce_mean( (_scale(model_values, target_lb, target_ub) - scaled_target)**2) optimization_result = optimizer_fn(loss_function, initial_position=initial_guess, tolerance=tolerance, max_iterations=maximum_iterations) calibrated_parameters = optimization_result.position mean_reversion_calibrated = piecewise.PiecewiseConstantFunc( jump_locations=[], values=_to_constrained(calibrated_parameters[:num_mean_reversion], mr_lb, mr_ub), dtype=dtype) volatility_calibrated = piecewise.PiecewiseConstantFunc( jump_locations=volatility.jump_locations(), values=_to_constrained(calibrated_parameters[num_mean_reversion:], vol_lb, vol_ub), dtype=dtype) calibrated_model = one_factor.HullWhiteModel1F( mean_reversion=mean_reversion_calibrated, volatility=volatility_calibrated, initial_discount_rate_fn=reference_rate_fn, dtype=dtype) return (calibrated_model, optimization_result.converged, optimization_result.num_iterations)
def calibration_from_swaptions(*, prices, expiries, floating_leg_start_times, floating_leg_end_times, fixed_leg_payment_times, floating_leg_daycount_fractions, fixed_leg_daycount_fractions, fixed_leg_coupon, reference_rate_fn, mean_reversion, volatility, notional=None, is_payer_swaption=None, use_analytic_pricing=True, num_samples=1, random_type=None, seed=None, skip=0, time_step=None, volatility_based_calibration=True, optimizer_fn=None, mean_reversion_lower_bound=0.001, mean_reversion_upper_bound=0.5, volatility_lower_bound=0.00001, volatility_upper_bound=0.1, tolerance=1e-6, maximum_iterations=50, dtype=None, name=None): """Calibrates the Hull-White model using European Swaptions. This function estimates the mean-reversion rate and volatility parameters of a Hull-White 1-factor model using a set of European swaption prices as the target. The calibration is performed using least-squares optimization where the loss function minimizes the squared error between the target swaption prices and the model implied swaption prices. #### Example The example shows how to calibrate a Hull-White model with constant mean reversion rate and constant volatility. ````python import numpy as np import tensorflow.compat.v2 as tf import tf_quant_finance as tff dtype = tf.float64 mean_reversion = [0.03] volatility = [0.01] expiries = np.array( [0.5, 0.5, 1.0, 1.0, 2.0, 2.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 10., 10.]) float_leg_start_times = np.array([ [0.5, 1.0, 1.5, 2.0, 2.5, 2.5, 2.5, 2.5, 2.5, 2.5], # 6M x 2Y swap [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0], # 6M x 5Y swap [1.0, 1.5, 2.0, 2.5, 3.0, 3.0, 3.0, 3.0, 3.0, 3.0], # 1Y x 2Y swap [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5], # 1Y x 5Y swap [2.0, 2.5, 3.0, 3.5, 4.0, 4.0, 4.0, 4.0, 4.0, 4.0], # 2Y x 2Y swap [2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5], # 2Y x 5Y swap [3.0, 3.5, 4.0, 4.5, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0], # 3Y x 2Y swap [3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5], # 3Y x 5Y swap [4.0, 4.5, 5.0, 5.5, 6.0, 6.0, 6.0, 6.0, 6.0, 6.0], # 4Y x 2Y swap [4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5], # 4Y x 5Y swap [5.0, 5.5, 6.0, 6.5, 7.0, 7.0, 7.0, 7.0, 7.0, 7.0], # 5Y x 2Y swap [5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5], # 5Y x 5Y swap [10.0, 10.5, 11.0, 11.5, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0], # 10Y x 2Y swap [10.0, 10.5, 11.0, 11.5, 12.0, 12.5, 13.0, 13.5, 14.0, 14.5] # 10Y x 5Y swap ]) float_leg_end_times = float_leg_start_times + 0.5 max_maturities = np.array( [2.5, 5.5, 3.0, 6.0, 4., 7., 5., 8., 6., 9., 7., 10., 12., 15.]) for i in range(float_leg_end_times.shape[0]): float_leg_end_times[i] = np.clip( float_leg_end_times[i], 0.0, max_maturities[i]) fixed_leg_payment_times = float_leg_end_times float_leg_daycount_fractions = ( float_leg_end_times - float_leg_start_times) fixed_leg_daycount_fractions = float_leg_daycount_fractions fixed_leg_coupon = 0.01 * np.ones_like(fixed_leg_payment_times) zero_rate_fn = lambda x: 0.01 * tf.ones_like(x, dtype=dtype) prices = tff.models.hull_white.swaption_price( expiries=expiries, floating_leg_start_times=float_leg_start_times, floating_leg_end_times=float_leg_end_times, fixed_leg_payment_times=fixed_leg_payment_times, floating_leg_daycount_fractions=float_leg_daycount_fractions, fixed_leg_daycount_fractions=fixed_leg_daycount_fractions, fixed_leg_coupon=fixed_leg_coupon, reference_rate_fn=zero_rate_fn, notional=100., dim=1, mean_reversion=mean_reversion, volatility=volatility, use_analytic_pricing=True, dtype=dtype) calibrated_model = tff.models.hull_white.calibration_from_swaptions( prices=prices[:, 0], expiries=expiries, floating_leg_start_times=float_leg_start_times, floating_leg_end_times=float_leg_end_times, fixed_leg_payment_times=fixed_leg_payment_times, floating_leg_daycount_fractions=float_leg_daycount_fractions, fixed_leg_daycount_fractions=fixed_leg_daycount_fractions, fixed_leg_coupon=fixed_leg_coupon, reference_rate_fn=zero_rate_fn, notional=100., mean_reversion=[0.01], # Initial guess for mean reversion rate volatility=[0.005], # Initial guess for volatility maximum_iterations=50, dtype=dtype) # Expected calibrated_model.mean_reversion.values(): [0.03] # Expected calibrated_model.volatility.values(): [0.01] ```` Args: prices: A rank 1 real `Tensor`. The prices of swaptions used for calibration. expiries: A real `Tensor` of same shape and dtype as `prices`. The time to expiration of the swaptions. floating_leg_start_times: A real `Tensor` of the same dtype as `expiries`. The times when accrual begins for each payment in the floating leg. The shape of this input should be `expiries.shape + [m]` where `m` denotes the number of floating payments in each leg. floating_leg_end_times: A real `Tensor` of the same dtype as `expiries`. The times when accrual ends for each payment in the floating leg. The shape of this input should be `expiries.shape + [m]` where `m` denotes the number of floating payments in each leg. fixed_leg_payment_times: A real `Tensor` of the same dtype as `expiries`. The payment times for each payment in the fixed leg. The shape of this input should be `expiries.shape + [n]` where `n` denotes the number of fixed payments in each leg. floating_leg_daycount_fractions: A real `Tensor` of the same dtype and compatible shape as `floating_leg_start_times`. The daycount fractions for each payment in the floating leg. fixed_leg_daycount_fractions: A real `Tensor` of the same dtype and compatible shape as `fixed_leg_payment_times`. The daycount fractions for each payment in the fixed leg. fixed_leg_coupon: A real `Tensor` of the same dtype and compatible shape as `fixed_leg_payment_times`. The fixed rate for each payment in the fixed leg. reference_rate_fn: A Python callable that accepts expiry time as a real `Tensor` and returns a `Tensor` of shape `input_shape + [dim]`. Returns the continuously compounded zero rate at the present time for the input expiry time. mean_reversion: A real positive scalar `Tensor` or an Python callable. The callable should satisfy the following: (a) A left-continuous piecewise constant object (e.g., `tff.math.piecewise.PiecewiseConstantFunc`) that has a property `is_piecewise_constant` set to `True`. In this case the object should have a method `jump_locations(self)` that returns a `Tensor` of shape `[num_jumps]` and `values(self)` that returns a `Tensor` of shape `[num_jumps + 1]`. The callable, `mean_reversion(t)` should return a `Tensor` of shape `t.shape`, where `t` is a rank 1 `Tensor` of the same `dtype` as the output. Corresponds to the mean reversion rate to be calibrated. The input `Tensor` or the `Tensor` `mean_reversion.values()` is also used as the initial point for calibration. volatility: A real positive scalar `Tensor` of the same `dtype` as `mean_reversion` or a callable with the same specs as above. Corresponds to the Hull-White volatility parameter to be calibrated. notional: An optional `Tensor` of same dtype and compatible shape as `strikes`specifying the notional amount for the underlying swap. Default value: None in which case the notional is set to 1. is_payer_swaption: A boolean `Tensor` of a shape compatible with `expiries`. Indicates whether the prices correspond to payer (if True) or receiver (if False) swaption. If not supplied, payer swaptions are assumed. use_analytic_pricing: A Python boolean specifying if swaption pricing is done analytically during calibration. Analytic valuation is only supported for constant `mean_reversion` and piecewise constant `volatility`. If the input is `False`, then valuation using Monte-Carlo simulations is performed. Default value: The default value is `True`. num_samples: Positive scalar `int32` `Tensor`. The number of simulation paths during Monte-Carlo valuation of swaptions. This input is ignored during analytic valuation. Default value: The default value is 1. random_type: Enum value of `RandomType`. The type of (quasi)-random number generator to use to generate the simulation paths. This input is relevant only for Monte-Carlo valuation and ignored during analytic valuation. Default value: `None` which maps to the standard pseudo-random numbers. seed: Seed for the random number generator. The seed is only relevant if `random_type` is one of `[STATELESS, PSEUDO, HALTON_RANDOMIZED, PSEUDO_ANTITHETIC, STATELESS_ANTITHETIC]`. For `PSEUDO`, `PSEUDO_ANTITHETIC` and `HALTON_RANDOMIZED` the seed should be an Python integer. For `STATELESS` and `STATELESS_ANTITHETIC `must be supplied as an integer `Tensor` of shape `[2]`. This input is relevant only for Monte-Carlo valuation and ignored during analytic valuation. Default value: `None` which means no seed is set. skip: `int32` 0-d `Tensor`. The number of initial points of the Sobol or Halton sequence to skip. Used only when `random_type` is 'SOBOL', 'HALTON', or 'HALTON_RANDOMIZED', otherwise ignored. Default value: `0`. time_step: Scalar real `Tensor`. Maximal distance between time grid points in Euler scheme. Relevant when Euler scheme is used for simulation. This input is ignored during analytic valuation. Default value: `None`. volatility_based_calibration: An optional Python boolean specifying whether calibration is performed using swaption implied volatilities. If the input is `True`, then the swaption prices are first converted to normal implied volatilities and calibration is performed by minimizing the error between input implied volatilities and model implied volatilities. Default value: True. optimizer_fn: Optional Python callable which implements the algorithm used to minimize the objective function during calibration. It should have the following interface: result = optimizer_fn(value_and_gradients_function, initial_position, tolerance, max_iterations) `value_and_gradients_function` is a Python callable that accepts a point as a real `Tensor` and returns a tuple of `Tensor`s of real dtype containing the value of the function and its gradient at that point. 'initial_position' is a real `Tensor` containing the starting point of the optimization, 'tolerance' is a real scalar `Tensor` for stopping tolerance for the procedure and `max_iterations` specifies the maximum number of iterations. `optimizer_fn` should return a namedtuple containing the items: `position` (a tensor containing the optimal value), `converged` (a boolean indicating whether the optimize converged according the specified criteria), `failed` (a boolean indicating if the optimization resulted in a failure), `num_iterations` (the number of iterations used), and `objective_value` ( the value of the objective function at the optimal value). The default value for `optimizer_fn` is None and conjugate gradient algorithm is used. mean_reversion_lower_bound: An optional scalar `Tensor` specifying the lower limit of mean reversion rate during calibration. Default value: 0.001. mean_reversion_upper_bound: An optional scalar `Tensor` specifying the upper limit of mean reversion rate during calibration. Default value: 0.5. volatility_lower_bound: An optional scalar `Tensor` specifying the lower limit of Hull White volatility during calibration. Default value: 0.00001 (0.1 basis points). volatility_upper_bound: An optional scalar `Tensor` specifying the upper limit of Hull White volatility during calibration. Default value: 0.1. tolerance: Scalar `Tensor` of real dtype. The absolute tolerance for terminating the iterations. Default value: 1e-6. maximum_iterations: Scalar positive int32 `Tensor`. The maximum number of iterations during the optimization. Default value: 50. dtype: The default dtype to use when converting values to `Tensor`s. Default value: `None` which means that default dtypes inferred by TensorFlow are used. name: Python string. The name to give to the ops created by this function. Default value: `None` which maps to the default name `hw_swaption_calibration`. Returns: A Tuple of three elements. The first element is an instance of `HullWhite1F` whose parameters are calibrated to the input swaption prices. The second and third elements contains the optimization status (whether the optimization algorithm succeeded in finding the optimal point based on the specified convergance criteria) and the number of iterations performed. """ name = name or 'hw_swaption_calibration' with tf.name_scope(name): prices = tf.convert_to_tensor(prices, dtype=dtype, name='prices') expiries = tf.convert_to_tensor(expiries, dtype=dtype, name='expiries') dtype = dtype or expiries.dtype float_leg_start_times = tf.convert_to_tensor( floating_leg_start_times, dtype=dtype, name='float_leg_start_times') float_leg_end_times = tf.convert_to_tensor(floating_leg_end_times, dtype=dtype, name='float_leg_end_times') fixed_leg_payment_times = tf.convert_to_tensor( fixed_leg_payment_times, dtype=dtype, name='fixed_leg_payment_times') fixed_leg_daycount_fractions = tf.convert_to_tensor( fixed_leg_daycount_fractions, dtype=dtype, name='fixed_leg_daycount_fractions') fixed_leg_coupon = tf.convert_to_tensor(fixed_leg_coupon, dtype=dtype, name='fixed_leg_coupon') notional = tf.convert_to_tensor(notional, dtype=dtype, name='notional') vol_lb = tf.convert_to_tensor(volatility_lower_bound, dtype=dtype) vol_ub = tf.convert_to_tensor(volatility_upper_bound, dtype=dtype) mr_lb = tf.convert_to_tensor(mean_reversion_lower_bound, dtype=dtype) mr_ub = tf.convert_to_tensor(mean_reversion_upper_bound, dtype=dtype) if not hasattr(mean_reversion, 'is_piecewise_constant'): mean_reversion = piecewise.PiecewiseConstantFunc( jump_locations=[], values=mean_reversion, dtype=dtype) if not hasattr(volatility, 'is_piecewise_constant'): volatility = piecewise.PiecewiseConstantFunc(jump_locations=[], values=volatility, dtype=dtype) if optimizer_fn is None: optimizer_fn = optimizer.conjugate_gradient_minimize if volatility_based_calibration: swap_rate, annuity = swap.ir_swap_par_rate_and_annuity( float_leg_start_times, float_leg_end_times, fixed_leg_payment_times, fixed_leg_daycount_fractions, reference_rate_fn) target_values = implied_vol( prices=prices / annuity / notional, strikes=fixed_leg_coupon[..., 0], expiries=expiries, forwards=swap_rate, is_call_options=is_payer_swaption, underlying_distribution=UnderlyingDistribution.NORMAL, dtype=dtype) else: target_values = prices target_lb = tf.constant(0.0, dtype=dtype) target_ub = tf.math.reduce_max(target_values) initial_guess = tf.concat([ _to_unconstrained(mean_reversion.values(), mr_lb, mr_ub), _to_unconstrained(volatility.values(), vol_lb, vol_ub) ], axis=0) num_mean_reversion = mean_reversion.values().shape.as_list()[0] scaled_target = _scale(target_values, target_lb, target_ub) @make_val_and_grad_fn def loss_function(x): """Loss function for the optimization.""" x_mr = _to_constrained(x[:num_mean_reversion], mr_lb, mr_ub) x_vol = _to_constrained(x[num_mean_reversion:], vol_lb, vol_ub) mean_reversion_param = piecewise.PiecewiseConstantFunc( jump_locations=[], values=x_mr, dtype=dtype) volatility_param = piecewise.PiecewiseConstantFunc( jump_locations=volatility.jump_locations(), values=x_vol, dtype=dtype) model_values = swaption.swaption_price( expiries=expiries, floating_leg_start_times=float_leg_start_times, floating_leg_end_times=float_leg_end_times, fixed_leg_payment_times=fixed_leg_payment_times, floating_leg_daycount_fractions=floating_leg_daycount_fractions, fixed_leg_daycount_fractions=fixed_leg_daycount_fractions, fixed_leg_coupon=fixed_leg_coupon, reference_rate_fn=reference_rate_fn, dim=1, mean_reversion=mean_reversion_param, volatility=volatility_param, notional=notional, is_payer_swaption=is_payer_swaption, use_analytic_pricing=use_analytic_pricing, num_samples=num_samples, random_type=random_type, seed=seed, skip=skip, time_step=time_step, dtype=dtype)[:, 0] if volatility_based_calibration: model_values = implied_vol( prices=model_values / annuity / notional, strikes=fixed_leg_coupon[..., 0], expiries=expiries, forwards=swap_rate, is_call_options=is_payer_swaption, underlying_distribution=UnderlyingDistribution.NORMAL, dtype=dtype) model_values = tf.where(tf.math.is_nan(model_values), tf.zeros_like(model_values), model_values) value = tf.math.reduce_sum( (_scale(model_values, target_lb, target_ub) - scaled_target)**2) return value optimization_result = optimizer_fn(loss_function, initial_position=initial_guess, tolerance=tolerance, max_iterations=maximum_iterations) calibrated_parameters = optimization_result.position mean_reversion_calibrated = piecewise.PiecewiseConstantFunc( jump_locations=[], values=_to_constrained(calibrated_parameters[:num_mean_reversion], mr_lb, mr_ub), dtype=dtype) volatility_calibrated = piecewise.PiecewiseConstantFunc( jump_locations=volatility.jump_locations(), values=_to_constrained(calibrated_parameters[num_mean_reversion:], vol_lb, vol_ub), dtype=dtype) calibrated_model = one_factor.HullWhiteModel1F( mean_reversion=mean_reversion_calibrated, volatility=volatility_calibrated, initial_discount_rate_fn=reference_rate_fn, dtype=dtype) return (calibrated_model, optimization_result.converged, optimization_result.num_iterations)
def __init__(self, dim, mean_reversion, volatility, initial_discount_rate_fn, corr_matrix=None, dtype=None, name=None): """Initializes the HJM model. Args: dim: A Python scalar which corresponds to the number of factors comprising the model. mean_reversion: A real positive `Tensor` of shape `[dim]`. Corresponds to the mean reversion rate of each factor. volatility: A real positive `Tensor` of the same `dtype` and shape as `mean_reversion` or a callable with the following properties: (a) The callable should accept a scalar `Tensor` `t` and returns a 1-D `Tensor` of shape `[dim]`. The function returns instantaneous volatility `sigma(t)`. When `volatility` is specified is a real `Tensor`, each factor is assumed to have a constant instantaneous volatility. Corresponds to the instantaneous volatility of each factor. initial_discount_rate_fn: A Python callable that accepts expiry time as a real `Tensor` of the same `dtype` as `mean_reversion` and returns a `Tensor` of shape `input_shape + dim`. Corresponds to the zero coupon bond yield at the present time for the input expiry time. corr_matrix: A `Tensor` of shape `[dim, dim]` and the same `dtype` as `mean_reversion`. Corresponds to the correlation matrix `Rho`. dtype: The default dtype to use when converting values to `Tensor`s. Default value: `None` which means that default dtypes inferred by TensorFlow are used. name: Python string. The name to give to the ops created by this class. Default value: `None` which maps to the default name `gaussian_hjm_model`. """ self._name = name or 'gaussian_hjm_model' with tf.name_scope(self._name): self._dtype = dtype or None self._dim = dim self._factors = dim def _instant_forward_rate_fn(t): t = tf.convert_to_tensor(t, dtype=self._dtype) def _log_zero_coupon_bond(x): r = tf.convert_to_tensor(initial_discount_rate_fn(x), dtype=self._dtype) return -r * x rate = -gradient.fwd_gradient( _log_zero_coupon_bond, t, use_gradient_tape=True, unconnected_gradients=tf.UnconnectedGradients.ZERO) return rate def _initial_discount_rate_fn(t): return tf.convert_to_tensor(initial_discount_rate_fn(t), dtype=self._dtype) self._instant_forward_rate_fn = _instant_forward_rate_fn self._initial_discount_rate_fn = _initial_discount_rate_fn self._mean_reversion = tf.convert_to_tensor(mean_reversion, dtype=dtype, name='mean_reversion') # Setup volatility if callable(volatility): self._volatility = volatility else: volatility = tf.convert_to_tensor(volatility, dtype=dtype) jump_locations = [[]] * dim volatility = tf.expand_dims(volatility, axis=-1) self._volatility = piecewise.PiecewiseConstantFunc( jump_locations=jump_locations, values=volatility, dtype=dtype) if corr_matrix is None: corr_matrix = tf.eye(dim, dim, dtype=self._dtype) self._rho = tf.convert_to_tensor(corr_matrix, dtype=dtype, name='rho') self._sqrt_rho = tf.linalg.cholesky(self._rho) # Volatility function def _vol_fn(t, state): """Volatility function of Gaussian-HJM.""" del state volatility = self._volatility(tf.expand_dims( t, -1)) # shape=(dim, 1) return self._sqrt_rho * volatility # Drift function def _drift_fn(t, state): """Drift function of Gaussian-HJM.""" x = state # shape = [self._factors, self._factors] y = self.state_y(tf.expand_dims(t, axis=-1))[..., 0] drift = tf.math.reduce_sum(y, axis=-1) - self._mean_reversion * x return drift self._exact_discretization_setup(dim) super(quasi_gaussian_hjm.QuasiGaussianHJM, self).__init__(dim, _drift_fn, _vol_fn, dtype, self._name)