def update_state(self, data): if self._has_input_vocabulary: raise ValueError( "Cannot adapt {} layer after setting a static vocabulary via init " "argument or `set_vocabulary`.".format( self.__class__.__name__)) data = self._standardize_inputs(data, self.vocabulary_dtype) if data.shape.rank == 0: data = tf.expand_dims(data, 0) if data.shape.rank == 1: # Expand dims on axis 0 for tf-idf. A 1-d tensor is a single document. data = tf.expand_dims(data, 0) tokens, counts = self._num_tokens(data) self.token_counts.insert(tokens, counts + self.token_counts.lookup(tokens)) if self.output_mode == TF_IDF: # Dedupe each row of our dataset. deduped_doc_data = tf.map_fn(lambda x: tf.unique(x)[0], data) # Flatten and count tokens. tokens, doc_counts = self._num_tokens(deduped_doc_data) self.token_document_counts.insert( tokens, doc_counts + self.token_document_counts.lookup(tokens)) if tf_utils.is_ragged(data): self.num_documents.assign_add(data.nrows()) else: self.num_documents.assign_add( tf.shape(data, out_type=tf.int64)[0])
def assert_mvn_target_conservation(event_size, batch_size, **kwargs): initialization = tfd.MultivariateNormalFullCovariance( loc=tf.zeros(event_size), covariance_matrix=tf.eye(event_size)).sample(batch_size, seed=4) samples, leapfrogs = run_nuts_chain(event_size, batch_size, num_steps=1, initial_state=initialization, **kwargs) answer = samples[0][-1] check_cdf_agrees = ( st.assert_multivariate_true_cdf_equal_on_projections_two_sample( answer, initialization, num_projections=100, false_fail_rate=1e-6)) check_sample_shape = tf1.assert_equal( tf.shape(input=answer)[0], batch_size) unique, _ = tf.unique(leapfrogs[0]) check_leapfrogs_vary = tf1.assert_greater_equal( tf.shape(input=unique)[0], 3) avg_leapfrogs = tf.math.reduce_mean(input_tensor=leapfrogs[0]) check_leapfrogs = tf1.assert_greater_equal( avg_leapfrogs, tf.constant(4, dtype=avg_leapfrogs.dtype)) movement = tf.linalg.norm(tensor=answer - initialization, axis=-1) # This movement distance (0.3) was copied from the univariate case. check_movement = tf1.assert_greater_equal( tf.reduce_mean(input_tensor=movement), 0.3) check_enough_power = tf1.assert_less( st.min_discrepancy_of_true_cdfs_detectable_by_dkwm_two_sample( batch_size, batch_size, false_fail_rate=1e-8, false_pass_rate=1e-6), 0.055) return (check_cdf_agrees, check_sample_shape, check_leapfrogs_vary, check_leapfrogs, check_movement, check_enough_power)
def assert_univariate_target_conservation(test, mk_target, step_size, stackless): # Sample count limited partly by memory reliably available on Forge. The test # remains reasonable even if the nuts recursion limit is severely curtailed # (e.g., 3 or 4 levels), so use that to recover some memory footprint and bump # the sample count. num_samples = int(5e4) num_steps = 1 target_d = mk_target() strm = tfp.util.SeedStream(salt='univariate_nuts_test', seed=1) # We wrap the initial values in `tf.identity` to avoid broken gradients # resulting from a bijector cache hit, since bijectors of the same # type/parameterization now share a cache. # TODO(b/72831017): Fix broken gradients caused by bijector caching. initialization = tf.identity(target_d.sample([num_samples], seed=strm())) def target(*args): # TODO(axch): Just use target_d.log_prob directly, and accept target_d # itself as an argument instead of a maker function. Blocked by # b/128932888. It would then also be nice not to eta-expand # target_d.log_prob; that was blocked by b/122414321, but maybe tfp's port # of value_and_gradients_function fixed that bug. return mk_target().log_prob(*args) operator = tfp.experimental.mcmc.NoUTurnSampler(target, step_size=step_size, max_tree_depth=3, use_auto_batching=True, stackless=stackless, unrolled_leapfrog_steps=2, seed=strm()) result, extra = tfp.mcmc.sample_chain(num_results=num_steps, num_burnin_steps=0, current_state=initialization, kernel=operator) # Note: sample_chain puts the chain history on top, not the (independent) # chains. test.assertAllEqual([num_steps, num_samples], result.shape) answer = result[0] check_cdf_agrees = st.assert_true_cdf_equal_by_dkwm(answer, target_d.cdf, false_fail_rate=1e-6) check_enough_power = tf1.assert_less( st.min_discrepancy_of_true_cdfs_detectable_by_dkwm( num_samples, false_fail_rate=1e-6, false_pass_rate=1e-6), 0.025) test.assertAllEqual([num_samples], extra.leapfrogs_taken[0].shape) unique, _ = tf.unique(extra.leapfrogs_taken[0]) check_leapfrogs_vary = tf1.assert_greater_equal( tf.shape(input=unique)[0], 3) avg_leapfrogs = tf.math.reduce_mean(input_tensor=extra.leapfrogs_taken[0]) check_leapfrogs = tf1.assert_greater_equal( avg_leapfrogs, tf.constant(4, dtype=avg_leapfrogs.dtype)) movement = tf.abs(answer - initialization) test.assertAllEqual([num_samples], movement.shape) # This movement distance (1 * step_size) was selected by reducing until 100 # runs with independent seeds all passed. check_movement = tf1.assert_greater_equal( tf.reduce_mean(input_tensor=movement), 1 * step_size) return (check_cdf_agrees, check_enough_power, check_leapfrogs_vary, check_leapfrogs, check_movement)
def _create_termstructure_maturities(fixed_leg_payment_times): """Create maturities needed for termstructure simulations.""" maturities = fixed_leg_payment_times maturities_shape = tf.shape(maturities) unique_maturities, _ = tf.unique(tf.reshape(maturities, shape=[-1])) unique_maturities = tf.sort(unique_maturities, name='sort_maturities') return maturities, unique_maturities, maturities_shape
def _scan_fn(*_): exchange = exchange_proposed_fn(num_replica, seed) flat_replicas = tf.reshape(exchange, [-1]) with tf.control_dependencies([ tf1.assert_equal( tf.size(input=flat_replicas), tf.size(input=tf.unique(flat_replicas)[0])), tf1.assert_greater_equal(flat_replicas, 0), tf1.assert_less(flat_replicas, num_replica), ]): return tf.shape(input=exchange)[0]
def _create_term_structure_maturities(fixed_leg_payment_times): """Create maturities needed for termstructure simulations.""" with tf.name_scope('create_termstructure_maturities'): maturities = fixed_leg_payment_times maturities_shape = tf.shape(maturities) # We should eventually remove tf.unique, but keeping it for now because # PDE solvers are not xla compatible in TFF currently. unique_maturities, _ = tf.unique(tf.reshape(maturities, shape=[-1])) unique_maturities = tf.sort(unique_maturities, name='sort_maturities') return maturities, unique_maturities, maturities_shape
def deduplicate_indexed_slices(indexed_slice: tf.IndexedSlices): """Sums `values` associated with any non-unique `indices`. Args: indexed_slice: An indexed slice with potentially duplicated indices. Returns: A tuple of (`summed_values`, `unique_indices`) where `unique_indices` is a de-duplicated version of `indices` and `summed_values` contains the sum of `values` slices associated with each unique index. """ values, indices = indexed_slice.values, indexed_slice.indices unique_indices, new_index_positions = tf.unique(indices) summed_values = tf.math.unsorted_segment_sum(values, new_index_positions, tf.shape(unique_indices)[0]) return summed_values, unique_indices
def collater_fn(batch: Dict[str, tf.Tensor]) -> Dict[str, tf.Tensor]: new_batch = mention_collater_fn(batch) # Only generate text identifiers and mention hashes for # the target (linked) mentions. new_batch['target_text_identifiers'] = tf.gather( new_batch['text_identifiers'], new_batch['mention_target_batch_positions']) new_batch[ 'target_mention_hashes'] = mention_preprocess_utils.modified_cantor_pairing( new_batch['mention_target_start_positions'], new_batch['target_text_identifiers']) seq_len = tf.shape(batch['text_ids'])[1] starts_far_from_passage_boundary = tf.greater_equal( new_batch['mention_target_start_positions'], min_distance_from_passage_boundary) ends_far_from_passage_boundary = tf.less( new_batch['mention_target_end_positions'], tf.cast(seq_len, new_batch['mention_target_end_positions'].dtype) - min_distance_from_passage_boundary) far_from_passage_boundary = tf.logical_and( starts_far_from_passage_boundary, ends_far_from_passage_boundary) far_from_passage_boundary = tf.cast( far_from_passage_boundary, dtype=new_batch['mention_target_weights'].dtype) new_batch['mention_target_weights'] = ( new_batch['mention_target_weights'] * far_from_passage_boundary) # Collect unique mention IDs per sample in the batch unique_mention_ids = [] # Mask-out not linked entities. dense_mention_ids = batch['dense_mention_ids'] * batch[ 'dense_mention_mask'] for i in range(bsz): unique_mention_ids_per_i = tf.unique(dense_mention_ids[i]).y unique_mention_ids_per_i = tf.cast(unique_mention_ids_per_i, tf.int32) unique_mention_ids_per_i = mention_preprocess_utils.dynamic_padding_1d( unique_mention_ids_per_i, max_mentions_per_sample) unique_mention_ids.append(unique_mention_ids_per_i) new_batch['unique_mention_ids'] = tf.stack(unique_mention_ids) return new_batch
def _prepare_grid(*, times, time_step, dtype): """Prepares grid of times for path generation. Args: times: Rank 1 `Tensor` of increasing positive real values. The times at which the path points are to be evaluated. time_step: Rank 0 real `Tensor`. Maximal distance between points in resulting grid. dtype: `tf.Dtype` of the input and output `Tensor`s. Returns: Tuple `(all_times, mask, time_points)`. `all_times` is a 1-D real `Tensor` containing all points from 'times` and the uniform grid of points between `[0, times[-1]]` with grid size equal to `time_step`. The `Tensor` is sorted in ascending order and may contain duplicates. `mask` is a boolean 1-D `Tensor` of the same shape as 'all_times', showing which elements of 'all_times' correspond to THE values from `times`. Guarantees that times[0]=0 and mask[0]=False. `time_indices`. An integer `Tensor` of the same shape as `times` indicating `times` indices in `all_times`. """ grid = tf.range(0.0, times[-1], time_step, dtype=dtype) all_times = tf.concat([times, grid], axis=0) # Remove duplicate points all_times = tf.unique(all_times).y # Sort sequence. Identify the time indices of interest # TODO(b/169400743): use tf.sort instead of argsort and casting when XLA # float64 support is extended for tf.sort args = tf.argsort(tf.cast(all_times, dtype=tf.float32)) all_times = tf.gather(all_times, args) time_indices = tf.searchsorted(all_times, times, out_type=tf.int32) # Create a boolean mask to identify the iterations that have to be recorded. mask_sparse = tf.sparse.SparseTensor(indices=tf.expand_dims(tf.cast( time_indices, dtype=tf.int64), axis=1), values=tf.fill(times.shape, True), dense_shape=all_times.shape) mask = tf.sparse.to_dense(mask_sparse) return all_times, mask, time_indices
def fit(self, X, y): self.X_train_ = X self.y = y self.classes = tf.unique(self.y)[0] self.start = np.array([0.] * 2, dtype='float64') if tf.math.greater(self.classes.shape, 2): raise ValueError( 'Only supports binary classification, y contain classes %s' % self.classes.shape) if self.optimizer is not None and self.kernel.feature_ndims > 0: def make_val_and_grad_fn(params): val_and_grad = tfp.math.value_and_gradient( self.log_marginal_likelihood, params) val_and_grad = (-val_and_grad[0], -val_and_grad[1]) return val_and_grad optim_results = tfp.optimizer.lbfgs_minimize( make_val_and_grad_fn, initial_position=self.start, f_relative_tolerance=0.0000001) self.log_marginal_likelihood_value_ = optim_results.objective_value self.converged = optim_results.converged if self.converged == False: warnings.warn("L-BFGS is not converged.") self.kernel._amplitude = tf.math.exp(optim_results.position[0]) self.kernel._length_scale = tf.math.exp(optim_results.position[1:]) self.K = self.kernel.matrix(self.X_train_, self.X_train_) _, _, (self.pi_, self.W_sr_, self.L_, _, _) = self.posterior_mode(self.K[0], return_temporaries=True) return self
def _create_pde_time_grid(exercise_times, time_step_fd, num_time_steps_fd, dtype): """Create PDE time grid.""" with tf.name_scope('create_pde_time_grid'): exercise_times, _ = tf.unique(tf.reshape(exercise_times, shape=[-1])) if num_time_steps_fd is not None: num_time_steps_fd = tf.convert_to_tensor( num_time_steps_fd, dtype=tf.int32, name='num_time_steps_fd') time_step_fd = tf.math.reduce_max(exercise_times) / tf.cast( num_time_steps_fd, dtype=dtype) if time_step_fd is None and num_time_steps_fd is None: num_time_steps_fd = 100 pde_time_grid, _, _ = utils.prepare_grid( times=exercise_times, time_step=time_step_fd, dtype=dtype, num_time_steps=num_time_steps_fd) pde_time_grid_dt = pde_time_grid[1:] - pde_time_grid[:-1] pde_time_grid_dt = tf.concat([[100.0], pde_time_grid_dt], axis=-1) return pde_time_grid, pde_time_grid_dt
def _create_pde_time_grid(exercise_times, time_step_fd, dtype): """Create PDE time grid.""" unique_exercise_times, _ = tf.unique(tf.reshape(exercise_times, shape=[-1])) longest_exercise_time = unique_exercise_times[-1] if time_step_fd is None: time_step_fd = longest_exercise_time / 100.0 pde_time_grid = tf.concat([ unique_exercise_times, tf.range(0.0, longest_exercise_time, time_step_fd, dtype=dtype) ], axis=0) # This time grid is now sorted and contains the Bermudan exercise times pde_time_grid = tf.sort(pde_time_grid, name='sort_pde_time_grid') pde_time_grid_dt = pde_time_grid[1:] - pde_time_grid[:-1] pde_time_grid_dt = tf.concat([[100.0], pde_time_grid_dt], axis=-1) # Remove duplicates. mask = tf.math.greater(pde_time_grid_dt, _PDE_TIME_GRID_TOL) pde_time_grid = tf.boolean_mask(pde_time_grid, mask) pde_time_grid_dt = tf.boolean_mask(pde_time_grid_dt, mask) return pde_time_grid, pde_time_grid_dt
def _prepare_grid(*, times, time_step, dtype): """Prepares grid of times for path generation. Args: times: Rank 1 `Tensor` of increasing positive real values. The times at which the path points are to be evaluated. time_step: Rank 0 real `Tensor`. Maximal distance between points in resulting grid. dtype: `tf.Dtype` of the input and output `Tensor`s. Returns: Tuple `(all_times, mask, time_points)`. `all_times` is a 1-D real `Tensor` containing all points from 'times` and the uniform grid of points between `[0, times[-1]]` with grid size equal to `time_step`. The `Tensor` is sorted in ascending order and may contain duplicates. `mask` is a boolean 1-D `Tensor` of the same shape as 'all_times', showing which elements of 'all_times' correspond to THE values from `times`. Guarantees that times[0]=0 and mask[0]=False. `time_indices`. An integer `Tensor` of the same shape as `times` indicating `times` indices in `all_times`. """ grid = tf.range(0.0, times[-1], time_step, dtype=dtype) all_times = tf.concat([grid, times], axis=0) mask = tf.concat([ tf.zeros_like(grid, dtype=tf.bool), tf.ones_like(times, dtype=tf.bool) ], axis=0) perm = tf.argsort(all_times, stable=True) all_times = tf.gather(all_times, perm) # Remove duplicate points all_times = tf.unique(all_times).y time_indices = tf.searchsorted(all_times, times) mask = tf.gather(mask, perm) return all_times, mask, time_indices
def _deduplicate_sparse_grad(self, grads): """Deduplicate sparse gradient. For sparse gradients, i.e., gradient is of type `tf.IndexedSlices`, it is possible that `gradient.indices` has duplicated indices. This function adds up values for the duplicated indices, and returns a `tf.IndexedSlices` with indices of unique values. """ processed_grads = [] for grad in grads: if isinstance(grad, tf.IndexedSlices): values = grad.values indices = grad.indices unique_indices, new_index_positions = tf.unique(indices) summed_values = tf.math.unsorted_segment_sum( values, new_index_positions, tf.shape(unique_indices)[0]) processed_grads.append( tf.IndexedSlices(summed_values, unique_indices, grad.dense_shape)) else: processed_grads.append(grad) return processed_grads
def discount_factors_and_bond_prices_from_samples( expiries, payment_times, sample_discount_curve_paths_fn, num_samples, times=None, curve_times=None, dtype=None): """Utility function to compute the discount factors and the bond prices. Args: expiries: A real `Tensor` of any and dtype. The time to expiration of the swaptions. The shape of this input determines the number (and shape) of swaptions to be priced and the shape of the output - e.g. if there are two swaptions, and there are 11 payment dates for each swaption, then the shape of `expiries` is [2, 11], with entries repeated along the second axis. payment_times: A real `Tensor` of same dtype and compatible shape with `expiries` - e.g. if there are two swaptions, and there are 11 payment dates for each swaption, then the shape of `payment_times` should be [2, 11] sample_discount_curve_paths_fn: Callable which takes the following args: 1) times: Rank 1 `Tensor` of positive real values, specifying the times at which the path points are to be evaluated. 2) curve_times: Rank 1 `Tensor` of positive real values, specifying the maturities at which the discount curve is to be computed at each simulation time. 3) num_samples: Positive scalar integer specifying the number of paths to draw. Returns two `Tensor`s, the first being a Rank-4 tensor of shape [num_samples, m, k, d] containing the simulated zero coupon bond curves, and the second being a `Tensor` of shape [num_samples, k, d] containing the simulated short rate paths. Here, m is the size of `curve_times`, k is the size of `times`, and d is the dimensionality of the paths. num_samples: Positive scalar `int32` `Tensor`. The number of simulation paths during Monte-Carlo valuation. times: An optional rank 1 `Tensor` of increasing positive real values. The times at which Monte Carlo simulations are performed. Default value: `None`. curve_times: An optional rank 1 `Tensor` of positive real values. The maturities at which spot discount curve is computed during simulations. Default value: `None`. 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. Returns: Two real tensors, `discount_factors` and `bond_prices`, both of shape [num_samples] + shape(payment_times) + [dim], where `dim` is the dimension of each path (e.g for a Hull-White with two models, dim==2; while for HJM dim==1 always.) """ if times is not None: sim_times = tf.convert_to_tensor(times, dtype=dtype) else: sim_times = tf.reshape(expiries, shape=[-1]) sim_times = tf.sort(sim_times, name='sort_sim_times') swaptionlet_shape = tf.shape(payment_times) tau = payment_times - expiries if curve_times is not None: curve_times = tf.convert_to_tensor(curve_times, dtype=dtype) else: curve_times = tf.reshape(tau, shape=[-1]) curve_times, _ = tf.unique(curve_times) curve_times = tf.sort(curve_times, name='sort_curve_times') p_t_tau, r_t, discount_factors = sample_discount_curve_paths_fn( times=sim_times, curve_times=curve_times, num_samples=num_samples) dim = tf.shape(p_t_tau)[-1] if discount_factors is None: dt = tf.concat(axis=0, values=[[0.0], sim_times[1:] - sim_times[:-1]]) dt = tf.expand_dims(tf.expand_dims(dt, axis=-1), axis=0) # Transpose before (and after) because we want the cumprod along axis=1 # but `cumsum_using_matvec` operates on the last axis. Also we use cumsum # and then exponentiate instead of taking cumprod of exponents for # efficiency. cumul_rdt = tf.transpose(utils.cumsum_using_matvec( tf.transpose(r_t * dt, perm=[0, 2, 1])), perm=[0, 2, 1]) discount_factors = tf.math.exp(-cumul_rdt) # Make discount factors the same shape as `p_t_tau`. This involves adding # an extra dimenstion (corresponding to `curve_times`). discount_factors = tf.expand_dims(discount_factors, axis=1) # tf.repeat is needed because we will use gather_nd later on this tensor. discount_factors_simulated = tf.repeat(discount_factors, tf.shape(p_t_tau)[1], axis=1) # `sim_times` and `curve_times` are sorted for simulation. We need to # select the indices corresponding to our input. sim_time_index = tf.searchsorted(sim_times, tf.reshape(expiries, [-1])) curve_time_index = tf.searchsorted(curve_times, tf.reshape(tau, [-1])) gather_index = _prepare_indices_ijjk(tf.range(0, num_samples), curve_time_index, sim_time_index, tf.range(0, dim)) # The shape after `gather_nd` will be `(num_samples*num_swaptionlets*dim,)` payoff_discount_factors_builder = tf.gather_nd(discount_factors_simulated, gather_index) # Reshape to `[num_samples] + swaptionlet.shape + [dim]` payoff_discount_factors = tf.reshape( payoff_discount_factors_builder, tf.concat([[num_samples], swaptionlet_shape, [dim]], axis=0)) payoff_bond_price_builder = tf.gather_nd(p_t_tau, gather_index) payoff_bond_price = tf.reshape( payoff_bond_price_builder, tf.concat([[num_samples], swaptionlet_shape, [dim]], axis=0)) return payoff_discount_factors, payoff_bond_price
def bond_option_price( *, strikes, expiries, maturities, discount_rate_fn, dim, mean_reversion, volatility, # TODO(b/159040541) Add correlation as an input. is_call_options=True, use_analytic_pricing=True, num_samples=1, random_type=None, seed=None, skip=0, time_step=None, dtype=None, name=None): """Calculates European bond option prices using the Hull-White model. Bond options are fixed income securities which give the holder a right to exchange at a future date (the option expiry) a zero coupon bond for a fixed price (the strike of the option). The maturity date of the bond is after the the expiry of the option. If `P(t,T)` denotes the price at time `t` of a zero coupon bond with maturity `T`, then the payoff from the option at option expiry, `T0`, is given by: ```None payoff = max(P(T0, T) - X, 0) ``` where `X` is the strike price of the option. #### Example ````python import numpy as np import tensorflow.compat.v2 as tf import tf_quant_finance as tff dtype = tf.float64 discount_rate_fn = lambda x: 0.01 * tf.ones_like(x, dtype=dtype) expiries = np.array([1.0]) maturities = np.array([5.0]) strikes = np.exp(-0.01 * maturities) / np.exp(-0.01 * expiries) price = tff.models.hull_white.bond_option_price( strikes=strikes, expiries=expiries, maturities=maturities, dim=1, mean_reversion=[0.03], volatility=[0.02], discount_rate_fn=discount_rate_fn, use_analytic_pricing=True, dtype=dtype) # Expected value: [[0.02817777]] ```` Args: strikes: A real `Tensor` of any shape and dtype. The strike price of the options. The shape of this input determines the number (and shape) of the options to be priced and the output. expiries: A real `Tensor` of the same dtype and compatible shape as `strikes`. The time to expiry of each bond option. maturities: A real `Tensor` of the same dtype and compatible shape as `strikes`. The time to maturity of the underlying zero coupon bonds. discount_rate_fn: A Python callable that accepts expiry time as a real `Tensor` and returns a `Tensor` of shape `input_shape + dim`. Computes the zero coupon bond yield at the present time for the input expiry time. dim: A Python scalar which corresponds to the number of Hull-White Models to be used for pricing. mean_reversion: A real positive `Tensor` of shape `[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 mean reversion rate. volatility: A real positive `Tensor` of the same `dtype` as `mean_reversion` or a callable with the same specs as above. Corresponds to the lond run price variance. is_call_options: A boolean `Tensor` of a shape compatible with `strikes`. Indicates whether the option is a call (if True) or a put (if False). If not supplied, call options are assumed. use_analytic_pricing: A Python boolean specifying if analytic valuation should be performed. 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. num_samples: Positive scalar `int32` `Tensor`. The number of simulation paths during Monte-Carlo valuation. 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`. 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 `hw_bond_option_price`. Returns: A `Tensor` of real dtype and shape `strikes.shape + [dim]` containing the computed option prices. """ name = name or 'hw_bond_option_price' if dtype is None: dtype = tf.convert_to_tensor([0.0]).dtype with tf.name_scope(name): 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') is_call_options = tf.convert_to_tensor(is_call_options, dtype=tf.bool, name='is_call_options') model = vector_hull_white.VectorHullWhiteModel( dim, mean_reversion=mean_reversion, volatility=volatility, initial_discount_rate_fn=discount_rate_fn, dtype=dtype) if use_analytic_pricing: return _analytic_valuation(discount_rate_fn, model, strikes, expiries, maturities, dim, is_call_options) if time_step is None: raise ValueError('`time_step` must be provided for simulation ' 'based bond option valuation.') sim_times, _ = tf.unique(tf.reshape(expiries, shape=[-1])) longest_expiry = tf.reduce_max(sim_times) sim_times, _ = tf.unique( tf.concat( [sim_times, tf.range(time_step, longest_expiry, time_step)], axis=0)) sim_times = tf.sort(sim_times, name='sort_sim_times') tau = maturities - expiries curve_times_builder, _ = tf.unique(tf.reshape(tau, shape=[-1])) curve_times = tf.sort(curve_times_builder, name='sort_curve_times') p_t_tau, r_t = model.sample_discount_curve_paths( times=sim_times, curve_times=curve_times, num_samples=num_samples, random_type=random_type, seed=seed, skip=skip) dt_builder = tf.concat([ tf.convert_to_tensor([0.0], dtype=dtype), sim_times[1:] - sim_times[:-1] ], axis=0) dt = tf.expand_dims(tf.expand_dims(dt_builder, axis=-1), axis=0) discount_factors_builder = tf.math.exp(-r_t * dt) # Transpose before (and after) because we want the cumprod along axis=1 # and `matvec` operates on the last axis. The shape before and after would # be `(num_samples, len(times), dim)` discount_factors_builder = tf.transpose( _cumprod_using_matvec( tf.transpose(discount_factors_builder, [0, 2, 1])), [0, 2, 1]) # make discount factors the same shape as `p_t_tau`. This involves adding # an extra dimenstion (corresponding to `curve_times`). discount_factors_builder = tf.expand_dims(discount_factors_builder, axis=1) discount_factors_simulated = tf.repeat(discount_factors_builder, p_t_tau.shape.as_list()[1], axis=1) # `sim_times` and `curve_times` are sorted for simulation. We need to # select the indices corresponding to our input. sim_time_index = tf.searchsorted(sim_times, tf.reshape(expiries, [-1])) curve_time_index = tf.searchsorted(curve_times, tf.reshape(tau, [-1])) gather_index = _prepare_indices(tf.range(0, num_samples), curve_time_index, sim_time_index, tf.range(0, dim)) # The shape after `gather_nd` would be (num_samples*num_strikes*dim,) payoff_discount_factors_builder = tf.gather_nd( discount_factors_simulated, gather_index) # Reshape to `[num_samples] + strikes.shape + [dim]` payoff_discount_factors = tf.reshape(payoff_discount_factors_builder, [num_samples] + strikes.shape + [dim]) payoff_bond_price_builder = tf.gather_nd(p_t_tau, gather_index) payoff_bond_price = tf.reshape(payoff_bond_price_builder, [num_samples] + strikes.shape + [dim]) is_call_options = tf.reshape( tf.broadcast_to(is_call_options, strikes.shape), [1] + strikes.shape + [1]) strikes = tf.reshape(strikes, [1] + strikes.shape + [1]) payoff = tf.where(is_call_options, tf.math.maximum(payoff_bond_price - strikes, 0.0), tf.math.maximum(strikes - payoff_bond_price, 0.0)) option_value = tf.math.reduce_mean(payoff_discount_factors * payoff, axis=0) return option_value
def bermudan_swaption_price(*, exercise_times, 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, dim, mean_reversion, volatility, notional=None, is_payer_swaption=None, lsm_basis=None, num_samples=100, random_type=None, seed=None, skip=0, time_step=None, dtype=None, name=None): """Calculates the price of Bermudan Swaptions using the Hull-White model. A Bermudan Swaption is a contract that gives the holder an option to enter a swap contract on a set of future exercise dates. The exercise dates are typically the fixing dates (or a subset thereof) of the underlying swap. If `T_N` denotes the final payoff date and `T_i, i = {1,...,n}` denote the set of exercise dates, then if the option is exercised at `T_i`, the holder is left with a swap with first fixing date equal to `T_i` and maturity `T_N`. Simulation based pricing of Bermudan swaptions is performed using the least squares Monte-carlo approach [1]. #### References: [1]: D. Brigo, F. Mercurio. Interest Rate Models-Theory and Practice. Second Edition. 2007. #### Example The example shows how value a batch of 5-no-call-1 and 5-no-call-2 swaptions using the Hull-White model. ````python import numpy as np import tensorflow.compat.v2 as tf import tf_quant_finance as tff dtype = tf.float64 exercise_swaption_1 = [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5] exercise_swaption_2 = [2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.0] exercise_times = [exercise_swaption_1, exercise_swaption_2] float_leg_start_times_1y = [1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5] float_leg_start_times_18m = [1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0] float_leg_start_times_2y = [2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.0] float_leg_start_times_30m = [2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.0, 5.0] float_leg_start_times_3y = [3.0, 3.5, 4.0, 4.5, 5.0, 5.0, 5.0, 5.0] float_leg_start_times_42m = [3.5, 4.0, 4.5, 5.0, 5.0, 5.0, 5.0, 5.0] float_leg_start_times_4y = [4.0, 4.5, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0] float_leg_start_times_54m = [4.5, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0] float_leg_start_times_5y = [5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0] float_leg_start_times_swaption_1 = [float_leg_start_times_1y, float_leg_start_times_18m, float_leg_start_times_2y, float_leg_start_times_30m, float_leg_start_times_3y, float_leg_start_times_42m, float_leg_start_times_4y, float_leg_start_times_54m] float_leg_start_times_swaption_2 = [float_leg_start_times_2y, float_leg_start_times_30m, float_leg_start_times_3y, float_leg_start_times_42m, float_leg_start_times_4y, float_leg_start_times_54m, float_leg_start_times_5y, float_leg_start_times_5y] float_leg_start_times = [float_leg_start_times_swaption_1, float_leg_start_times_swaption_2] float_leg_end_times = np.clip(np.array(float_leg_start_times) + 0.5, 0.0, 5.0) fixed_leg_payment_times = float_leg_end_times float_leg_daycount_fractions = (np.array(float_leg_end_times) - np.array(float_leg_start_times)) fixed_leg_daycount_fractions = float_leg_daycount_fractions fixed_leg_coupon = 0.011 * np.ones_like(fixed_leg_payment_times) zero_rate_fn = lambda x: 0.01 * tf.ones_like(x, dtype=dtype) price = bermudan_swaption_price( exercise_times=exercise_times, 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=[0.03], volatility=[0.01], num_samples=1000000, time_step=0.1, random_type=tff.math.random.RandomType.PSEUDO_ANTITHETIC, seed=0, dtype=dtype) # Expected value: [1.8913050118443016, 1.6618681421434984] # shape = (2,) ```` Args: exercise_times: A real `Tensor` of any shape `batch_shape + [num_exercise]` `and real dtype. The times corresponding to exercise dates of the swaptions. `num_exercise` corresponds to the number of exercise dates for the Bermudan swaption. The shape of this input determines the number (and shape) of Bermudan swaptions to be priced and the shape of the output. floating_leg_start_times: A real `Tensor` of the same dtype as `exercise_times`. The times when accrual begins for each payment in the floating leg upon exercise of the option. The shape of this input should be `exercise_times.shape + [m]` where `m` denotes the number of floating payments in each leg of the underlying swap until the swap maturity. floating_leg_end_times: A real `Tensor` of the same dtype as `exercise_times`. The times when accrual ends for each payment in the floating leg upon exercise of the option. The shape of this input should be `exercise_times.shape + [m]` where `m` denotes the number of floating payments in each leg of the underlying swap until the swap maturity. fixed_leg_payment_times: A real `Tensor` of the same dtype as `exercise_times`. The payment times for each payment in the fixed leg. The shape of this input should be `exercise_times.shape + [n]` where `n` denotes the number of fixed payments in each leg of the underlying swap until the swap maturity. 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. dim: A Python scalar which corresponds to the number of Hull-White Models to be used for pricing. mean_reversion: A real positive `Tensor` of shape `[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 mean reversion rate. volatility: A real positive `Tensor` of the same `dtype` as `mean_reversion` or a callable with the same specs as above. Corresponds to the lond run price variance. 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 swaption is a payer (if True) or a receiver (if False) swaption. If not supplied, payer swaptions are assumed. lsm_basis: A Python callable specifying the basis to be used in the LSM algorithm. The callable must accept a `Tensor`s of shape `[num_samples, dim]` and output `Tensor`s of shape `[m, num_samples]` where `m` is the nimber of basis functions used. Default value: `None`, in which case a polynomial basis of order 2 is used. num_samples: Positive scalar `int32` `Tensor`. The number of simulation paths during Monte-Carlo valuation. This input is ignored during analytic valuation. Default value: The default value is 100. random_type: Enum value of `RandomType`. The type of (quasi)-random number generator to use to generate the simulation paths. 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]`. 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. Default value: `None`. 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_bermudan_swaption_price`. Returns: A `Tensor` of real dtype and shape batch_shape + [dim] containing the computed swaption prices. Raises: (a) `ValueError` if exercise_times.rank is less than floating_leg_start_times.rank - 1, which would mean exercise times are not specified for all swaptions. (b) `ValueError` if `time_step` is not specified for Monte-Carlo simulations. (c) `ValueError` if `dim` > 1. """ if dim > 1: raise ValueError('dim > 1 is currently not supported.') name = name or 'hw_bermudan_swaption_price' del floating_leg_daycount_fractions, floating_leg_start_times del floating_leg_end_times with tf.name_scope(name): exercise_times = tf.convert_to_tensor( exercise_times, dtype=dtype, name='exercise_times') dtype = dtype or exercise_times.dtype 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') if is_payer_swaption is None: is_payer_swaption = True is_payer_swaption = tf.convert_to_tensor( is_payer_swaption, dtype=tf.bool, name='is_payer_swaption') if lsm_basis is None: basis_fn = lsm_v2.make_polynomial_basis(2) else: basis_fn = lsm_basis batch_shape = exercise_times.shape.as_list()[:-1] or [1] unique_exercise_times, exercise_time_index = tf.unique( tf.reshape(exercise_times, shape=[-1])) exercise_time_index = tf.reshape( exercise_time_index, shape=exercise_times.shape) # Add a dimension corresponding to multiple cashflows in a swap if exercise_times.shape.rank == fixed_leg_payment_times.shape.rank - 1: exercise_times = tf.expand_dims(exercise_times, axis=-1) elif exercise_times.shape.rank < fixed_leg_payment_times.shape.rank - 1: raise ValueError('Swaption exercise times not specified for all ' 'swaptions in the batch. Expected rank ' '{} but received {}.'.format( fixed_leg_payment_times.shape.rank - 1, exercise_times.shape.rank)) exercise_times = tf.repeat( exercise_times, fixed_leg_payment_times.shape.as_list()[-1], axis=-1) # Monte-Carlo pricing model = vector_hull_white.VectorHullWhiteModel( dim, mean_reversion, volatility, initial_discount_rate_fn=reference_rate_fn, dtype=dtype) if time_step is None: raise ValueError('`time_step` must be provided for LSM valuation.') sim_times = unique_exercise_times longest_exercise_time = sim_times[-1] sim_times, _ = tf.unique(tf.concat([sim_times, tf.range( time_step, longest_exercise_time, time_step)], axis=0)) sim_times = tf.sort(sim_times, name='sort_sim_times') maturities = fixed_leg_payment_times maturities_shape = maturities.shape tau = maturities - exercise_times curve_times_builder, _ = tf.unique(tf.reshape(tau, shape=[-1])) curve_times = tf.sort(curve_times_builder, name='sort_curve_times') # Simulate short rates and discount factors. p_t_tau, r_t = model.sample_discount_curve_paths( times=sim_times, curve_times=curve_times, num_samples=num_samples, random_type=random_type, seed=seed, skip=skip) dt = tf.concat( [tf.convert_to_tensor([0.0], dtype=dtype), sim_times[1:] - sim_times[:-1]], axis=0) dt = tf.expand_dims(tf.expand_dims(dt, axis=-1), axis=0) discount_factors_builder = tf.math.exp(-r_t * dt) # Transpose before (and after) because we want the cumprod along axis=1 # and `matvec` operates on the last axis. discount_factors_builder = tf.transpose( utils.cumprod_using_matvec( tf.transpose(discount_factors_builder, [0, 2, 1])), [0, 2, 1]) # make discount factors the same shape as `p_t_tau`. This involves adding # an extra dimenstion (corresponding to `curve_times`). discount_factors_builder = tf.expand_dims( discount_factors_builder, axis=1) # tf.repeat is needed because we will use gather_nd later on this tensor. discount_factors_simulated = tf.repeat( discount_factors_builder, p_t_tau.shape.as_list()[1], axis=1) # `sim_times` and `curve_times` are sorted for simulation. We need to # select the indices corresponding to our input. sim_time_index = tf.searchsorted( sim_times, tf.reshape(exercise_times, [-1])) curve_time_index = tf.searchsorted(curve_times, tf.reshape(tau, [-1])) gather_index = _prepare_indices( tf.range(0, num_samples), curve_time_index, sim_time_index, tf.range(0, dim)) # TODO(b/167421126): Replace `tf.gather_nd` with `tf.gather`. payoff_bond_price_builder = tf.gather_nd(p_t_tau, gather_index) payoff_bond_price = tf.reshape( payoff_bond_price_builder, [num_samples] + maturities_shape + [dim]) # Add an axis corresponding to `dim` fixed_leg_pv = tf.expand_dims( fixed_leg_coupon * fixed_leg_daycount_fractions, axis=-1) * payoff_bond_price # Sum fixed coupon payments within each swap to calculate the swap payoff # at each exercise time. fixed_leg_pv = tf.math.reduce_sum(fixed_leg_pv, axis=-2) float_leg_pv = 1.0 - payoff_bond_price[..., -1, :] payoff_swap = float_leg_pv - fixed_leg_pv payoff_swap = tf.where(is_payer_swaption, payoff_swap, -1.0 * payoff_swap) # Get the short rate simulations for the set of unique exercise times sim_time_index = tf.searchsorted(sim_times, unique_exercise_times) short_rate = tf.gather(r_t, sim_time_index, axis=1) # Currently the payoffs are computed on exercise times of each option. # They need to be mapped to the short rate simulation times, which is a # union of all exercise times. is_exercise_time, payoff_swap = _map_payoff_to_sim_times( exercise_time_index, payoff_swap, num_samples) # Transpose so that `time_index` is the leading dimension # (for XLA compatibility) perm = [is_exercise_time.shape.rank - 1] + list( range(is_exercise_time.shape.rank - 1)) is_exercise_time = tf.transpose(is_exercise_time, perm=perm) payoff_swap = tf.transpose(payoff_swap, perm=perm) # Time to call LSM def _payoff_fn(rt, time_index): del rt result = tf.where(is_exercise_time[time_index] > 0, tf.nn.relu(payoff_swap[time_index]), 0.0) return tf.reshape(result, shape=[num_samples] + batch_shape) discount_factors_simulated = tf.gather( discount_factors_simulated, sim_time_index, axis=2) option_value = lsm_v2.least_square_mc( short_rate, tf.range(0, tf.shape(short_rate)[1]), _payoff_fn, basis_fn, discount_factors=discount_factors_simulated[:, -1:, :, 0], dtype=dtype) return notional * option_value
def swaption_price(*, 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, dim, 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, dtype=None, name=None): """Calculates the price of European Swaptions using the Hull-White model. A European Swaption is a contract that gives the holder an option to enter a swap contract at a future date at a prespecified fixed rate. A swaption that grants the holder to pay fixed rate and receive floating rate is called a payer swaption while the swaption that grants the holder to receive fixed and pay floating payments is called the receiver swaption. Typically the start date (or the inception date) of the swap concides with the expiry of the swaption. Mid-curve swaptions are currently not supported (b/160061740). Analytic pricing of swaptions is performed using the Jamshidian decomposition [1]. #### References: [1]: D. Brigo, F. Mercurio. Interest Rate Models-Theory and Practice. Second Edition. 2007. #### Example The example shows how value a batch of 1y x 1y and 1y x 2y swaptions using the Hull-White model. ````python import numpy as np import tensorflow.compat.v2 as tf import tf_quant_finance as tff dtype = tf.float64 expiries = [1.0, 1.0] float_leg_start_times = [[1.0, 1.25, 1.5, 1.75, 2.0, 2.0, 2.0, 2.0], [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75]] float_leg_end_times = [[1.25, 1.5, 1.75, 2.0, 2.0, 2.0, 2.0, 2.0], [1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0]] fixed_leg_payment_times = [[1.25, 1.5, 1.75, 2.0, 2.0, 2.0, 2.0, 2.0], [1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0]] float_leg_daycount_fractions = [[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]] fixed_leg_daycount_fractions = [[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]] fixed_leg_coupon = [[0.011, 0.011, 0.011, 0.011, 0.0, 0.0, 0.0, 0.0], [0.011, 0.011, 0.011, 0.011, 0.011, 0.011, 0.011, 0.011]] zero_rate_fn = lambda x: 0.01 * tf.ones_like(x, dtype=dtype) price = 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=[0.03], volatility=[0.02], dtype=dtype) # Expected value: [[0.7163243383624043], [1.4031415262337608]] # shape = (2,1) ```` Args: expiries: A real `Tensor` of any shape and dtype. The time to expiration of the swaptions. The shape of this input determines the number (and shape) of swaptions to be priced and the shape of the output. 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. dim: A Python scalar which corresponds to the number of Hull-White Models to be used for pricing. mean_reversion: A real positive `Tensor` of shape `[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 mean reversion rate. volatility: A real positive `Tensor` of the same `dtype` as `mean_reversion` or a callable with the same specs as above. Corresponds to the lond run price variance. 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 swaption is a payer (if True) or a receiver (if False) swaption. If not supplied, payer swaptions are assumed. use_analytic_pricing: A Python boolean specifying if analytic valuation should be performed. 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. 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`. 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_price`. Returns: A `Tensor` of real dtype and shape expiries.shape + [dim] containing the computed swaption prices. For swaptions that have. reset in the past (expiries<0), the function sets the corresponding option prices to 0.0. """ # TODO(b/160061740): Extend the functionality to support mid-curve swaptions. name = name or 'hw_swaption_price' del floating_leg_daycount_fractions with tf.name_scope(name): 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') notional = tf.expand_dims( tf.broadcast_to(notional, expiries.shape), axis=-1) if is_payer_swaption is None: is_payer_swaption = True is_payer_swaption = tf.convert_to_tensor( is_payer_swaption, dtype=tf.bool, name='is_payer_swaption') output_shape = expiries.shape.as_list() + [dim] # Add a dimension corresponding to multiple cashflows in a swap if expiries.shape.rank == fixed_leg_payment_times.shape.rank - 1: expiries = tf.expand_dims(expiries, axis=-1) elif expiries.shape.rank < fixed_leg_payment_times.shape.rank - 1: raise ValueError('Swaption expiries not specified for all swaptions ' 'in the batch. Expected rank {} but received {}.'.format( fixed_leg_payment_times.shape.rank - 1, expiries.shape.rank)) # Expected shape: batch_shape + [m], same as fixed_leg_payment_times.shape # We need to explicitly use tf.repeat because we need to price # batch_shape + [m] bond options with different strikes along the last # dimension. expiries = tf.repeat( expiries, fixed_leg_payment_times.shape.as_list()[-1], axis=-1) if use_analytic_pricing: return _analytic_valuation(expiries, float_leg_start_times, float_leg_end_times, fixed_leg_payment_times, fixed_leg_daycount_fractions, fixed_leg_coupon, reference_rate_fn, dim, mean_reversion, volatility, notional, is_payer_swaption, output_shape, dtype, name + '_analytic_valyation') # Monte-Carlo pricing model = vector_hull_white.VectorHullWhiteModel( dim, mean_reversion, volatility, initial_discount_rate_fn=reference_rate_fn, dtype=dtype) if time_step is None: raise ValueError('`time_step` must be provided for simulation ' 'based bond option valuation.') sim_times, _ = tf.unique(tf.reshape(expiries, shape=[-1])) longest_expiry = tf.reduce_max(sim_times) sim_times, _ = tf.unique(tf.concat([sim_times, tf.range( time_step, longest_expiry, time_step)], axis=0)) sim_times = tf.sort(sim_times, name='sort_sim_times') maturities = fixed_leg_payment_times swaptionlet_shape = maturities.shape tau = maturities - expiries curve_times_builder, _ = tf.unique(tf.reshape(tau, shape=[-1])) curve_times = tf.sort(curve_times_builder, name='sort_curve_times') p_t_tau, r_t = model.sample_discount_curve_paths( times=sim_times, curve_times=curve_times, num_samples=num_samples, random_type=random_type, seed=seed, skip=skip) dt = tf.concat( [tf.convert_to_tensor([0.0], dtype=dtype), sim_times[1:] - sim_times[:-1]], axis=0) dt = tf.expand_dims(tf.expand_dims(dt, axis=-1), axis=0) discount_factors_builder = tf.math.exp(-r_t * dt) # Transpose before (and after) because we want the cumprod along axis=1 # and `matvec` operates on the last axis. discount_factors_builder = tf.transpose( utils.cumprod_using_matvec( tf.transpose(discount_factors_builder, [0, 2, 1])), [0, 2, 1]) # make discount factors the same shape as `p_t_tau`. This involves adding # an extra dimenstion (corresponding to `curve_times`). discount_factors_builder = tf.expand_dims( discount_factors_builder, axis=1) # tf.repeat is needed because we will use gather_nd later on this tensor. discount_factors_simulated = tf.repeat( discount_factors_builder, p_t_tau.shape.as_list()[1], axis=1) # `sim_times` and `curve_times` are sorted for simulation. We need to # select the indices corresponding to our input. sim_time_index = tf.searchsorted(sim_times, tf.reshape(expiries, [-1])) curve_time_index = tf.searchsorted(curve_times, tf.reshape(tau, [-1])) gather_index = _prepare_indices( tf.range(0, num_samples), curve_time_index, sim_time_index, tf.range(0, dim)) # The shape after `gather_nd` will be `(num_samples*num_swaptionlets*dim,)` payoff_discount_factors_builder = tf.gather_nd( discount_factors_simulated, gather_index) # Reshape to `[num_samples] + swaptionlet.shape + [dim]` payoff_discount_factors = tf.reshape( payoff_discount_factors_builder, [num_samples] + swaptionlet_shape + [dim]) payoff_bond_price_builder = tf.gather_nd(p_t_tau, gather_index) payoff_bond_price = tf.reshape( payoff_bond_price_builder, [num_samples] + swaptionlet_shape + [dim]) # Add an axis corresponding to `dim` fixed_leg_pv = tf.expand_dims( fixed_leg_coupon * fixed_leg_daycount_fractions, axis=-1) * payoff_bond_price # Sum fixed coupon payments within each swap fixed_leg_pv = tf.math.reduce_sum(fixed_leg_pv, axis=-2) float_leg_pv = 1.0 - payoff_bond_price[..., -1, :] payoff_swap = payoff_discount_factors[..., -1, :] * ( float_leg_pv - fixed_leg_pv) payoff_swap = tf.where(is_payer_swaption, payoff_swap, -1.0 * payoff_swap) payoff_swaption = tf.math.maximum(payoff_swap, 0.0) option_value = tf.reshape( tf.math.reduce_mean(payoff_swaption, axis=0), output_shape) return notional * option_value
def calibration_from_swaptions( *, prices: types.RealTensor, expiries: types.RealTensor, floating_leg_start_times: types.RealTensor, floating_leg_end_times: types.RealTensor, fixed_leg_payment_times: types.RealTensor, floating_leg_daycount_fractions: types.RealTensor, fixed_leg_daycount_fractions: types.RealTensor, fixed_leg_coupon: types.RealTensor, reference_rate_fn: Callable[..., types.RealTensor], num_hjm_factors: types.RealTensor, mean_reversion: types.RealTensor, volatility: types.RealTensor, notional: types.RealTensor = None, is_payer_swaption: types.BoolTensor = None, swaption_valuation_method: vm.ValuationMethod = None, num_samples: types.IntTensor = 1, random_type: random.RandomType = None, seed: types.IntTensor = None, skip: types.IntTensor = 0, times: types.RealTensor = None, time_step: types.RealTensor = None, num_time_steps: types.IntTensor = None, curve_times: types.RealTensor = None, time_step_finite_difference: types.RealTensor = None, num_grid_points_finite_difference: types.IntTensor = 101, volatility_based_calibration: bool = True, calibrate_correlation: bool = True, optimizer_fn: Callable[..., types.RealTensor] = None, mean_reversion_lower_bound: types.RealTensor = 0.001, mean_reversion_upper_bound: types.RealTensor = 0.5, volatility_lower_bound: types.RealTensor = 0.00001, volatility_upper_bound: types.RealTensor = 0.1, tolerance: types.RealTensor = 1e-6, maximum_iterations: types.IntTensor = 50, dtype: tf.DType = None, name: str = None) -> Tuple[CalibrationResult, types.BoolTensor, types.IntTensor]: """Calibrates a batch of HJM models using European Swaption prices. This function estimates the mean-reversion rates, volatility and correlation parameters of a multi factor HJM 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 (or volatilities) and the model implied swaption prices (or volatilities). The current calibration supports constant mean reversion, volatility and correlation parameters. #### Example The example shows how to calibrate a Two factor HJM 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 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) notional = 1.0 prices = np.array([ 0.42919881, 0.98046542, 0.59045074, 1.34909391, 0.79491583, 1.81768802, 0.93210461, 2.13625342, 1.05114573, 2.40921088, 1.12941064, 2.58857507, 1.37029637, 3.15081683]) (calibrated_mr, calibrated_vol, calibrated_corr), _, _ = ( tff.models.hjm.calibration_from_swaptions( prices=prices, 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, 0.01], # Initial guess for mean reversion rate volatility=[0.005, 0.004], # Initial guess for volatility volatility_based_calibration=True, calibrate_correlation=True, num_samples=2000, time_step=0.1, random_type=random.RandomType.STATELESS_ANTITHETIC, seed=[0,0], maximum_iterations=50, dtype=dtype)) # Expected calibrated_mr: [0.00621303, 0.3601772] # Expected calibrated_vol: [0.00586125, 0.00384013] # Expected correlation: 0.65126492 # Prices using calibrated model: [ 0.42939121, 0.95362327, 0.59186236, 1.32622752, 0.79575526, 1.80457544, 0.93909176, 2.14336776, 1.04132595, 2.39385229, 1.11770416, 2.58809336, 1.39557389, 3.29306317] ```` Args: prices: An N-D real `Tensor` of shape `batch_shape + [k]`. `batch_shape` is the shape of the batch of models to calibrate and `k` is the number of swaptions per calibration. The input represents 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 `prices`. 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 `prices`. 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 `prices`. 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`. Returns the continuously compounded zero rate at the present time for the input expiry time. num_hjm_factors: A Python scalar which corresponds to the number of factors in the batch of calibrated HJM models. mean_reversion: A real positive `Tensor` of same dtype as `prices` and shape `batch_shape + [num_hjm_factors]`. Corresponds to the initial values of the mean reversion rates of the factors for calibration. volatility: A real positive `Tensor` of the same `dtype` and shape as `mean_reversion`. Corresponds to the initial values of the volatility of the factors for calibration. 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. swaption_valuation_method: An enum of type `valuation_method.ValuationMethod` specifying the method to be used for swaption valuation during calibration. Currently the valuation is supported using `MONTE_CARLO` and `FINITE_DIFFERENCE` methods. Valuation using finite difference is only supported for Gaussian HJM models, i.e. for models with constant mean-reversion rate and time-dependent volatility. Default value: `valuation_method.ValuationMethod.MONTE_CARLO`, in which case swaption valuation is done using Monte Carlo simulations. 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`. times: An optional rank 1 `Tensor` of increasing positive real values. The times at which Monte Carlo simulations are performed. Relevant when swaption valuation is done using Monte Calro simulations. Default value: `None` in which case simulation times are computed based on either `time_step` or `num_time_steps` inputs. 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`. num_time_steps: An optional scalar integer `Tensor` - a total number of time steps during Monte Carlo simulations. The maximal distance betwen points in grid is bounded by `times[-1] / (num_time_steps - times.shape[0])`. Either this or `time_step` should be supplied when the valuation method is Monte Carlo. Default value: `None`. curve_times: An optional rank 1 `Tensor` of positive real values. The maturities at which spot discount curve is computed during simulations. Default value: `None` in which case `curve_times` is computed based on swaption expities and `fixed_leg_payments_times` inputs. time_step_finite_difference: Scalar real `Tensor`. Spacing between time grid points in finite difference discretization. This input is only relevant for valuation using finite difference. Default value: `None`, in which case a `time_step` corresponding to 100 discrete steps is used. num_grid_points_finite_difference: Scalar real `Tensor`. Number of spatial grid points for discretization. This input is only relevant for valuation using finite difference. Default value: 100. 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. calibrate_correlation: An optional Python boolean specifying if the correlation matrix between HJM factors should calibrated. If the input is `False`, then the model is calibrated assuming that the HJM factors are uncorrelated. 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 volatility during calibration. Default value: 0.00001 (0.1 basis points). volatility_upper_bound: An optional scalar `Tensor` specifying the upper limit of 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 `hjm_swaption_calibration`. Returns: A Tuple of three elements: * The first element is an instance of `CalibrationResult` whose parameters are calibrated to the input swaption prices. * A `Tensor` of optimization status for each batch element (whether the optimization algorithm has found the optimal point based on the specified convergance criteria). * A `Tensor` containing the number of iterations performed by the optimization algorithm. """ del floating_leg_daycount_fractions name = name or 'hjm_swaption_calibration' with tf.name_scope(name): prices = tf.convert_to_tensor(prices, dtype=dtype, name='prices') dtype = dtype or prices.dtype expiries = tf.convert_to_tensor(expiries, dtype=dtype, name='expiries') 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') if times is None: times, _ = tf.unique(tf.reshape(expiries, [-1])) times = tf.sort(times, name='sort_times') else: times = tf.convert_to_tensor(times, dtype=dtype) if curve_times is None: tau = fixed_leg_payment_times - tf.expand_dims(expiries, axis=-1) curve_times, _ = tf.unique(tf.reshape(tau, [-1])) curve_times = tf.sort(curve_times) else: curve_times = tf.convert_to_tensor(curve_times, dtype=dtype) 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) theta_lb = tf.convert_to_tensor(0, dtype=dtype) theta_ub = tf.convert_to_tensor(_THETA_UB, dtype=dtype) mean_reversion = tf.convert_to_tensor(mean_reversion, dtype=dtype) volatility = tf.convert_to_tensor(volatility, dtype=dtype) swaption_valuation_method = ( swaption_valuation_method or vm.ValuationMethod.MONTE_CARLO) if optimizer_fn is None: optimizer_fn = optimizer.conjugate_gradient_minimize def _price_to_normal_vol(x, swap_rate, annuity): vols = implied_vol( prices=x / annuity / notional, strikes=fixed_leg_coupon[..., 0], expiries=expiries, forwards=swap_rate, is_call_options=is_payer_swaption, underlying_distribution=UnderlyingDistribution.NORMAL, dtype=dtype) return vols if volatility_based_calibration: batch_shape = tf.shape(prices)[:-1] batch_size = tf.math.reduce_prod(batch_shape) num_instruments = tf.shape(prices)[-1] 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) # Because we require `reference_rate_fn` to return a Tensor of shape # `[batch_shape] + t.shape`, we get cross product terms that we don't # need. The logic below takes `swap_rate` and `annuity` from shape # `[batch_shape, batch_shape, num_instruments]` to # `[batch_shape, num_instruments]` swap_rate = tf.reshape( swap_rate, [batch_size, batch_size, num_instruments]) annuity = tf.reshape( annuity, [batch_size, batch_size, num_instruments]) indices = tf.stack([tf.range(batch_size, dtype=tf.int32), tf.range(batch_size, dtype=tf.int32)], axis=-1) swap_rate = tf.gather_nd(swap_rate, indices) annuity = tf.gather_nd(annuity, indices) swap_rate = tf.reshape(swap_rate, tf.shape(prices)) annuity = tf.reshape(annuity, tf.shape(prices)) target_values = _price_to_normal_vol(prices, swap_rate, annuity) else: target_values = prices with tf.control_dependencies([target_values]): tf.debugging.assert_all_finite( target_values, 'Conversion to implied vols resulted in failure for ' 'input swaption prices.') target_lb = tf.constant(0.0, dtype=dtype) target_ub = tf.math.reduce_max(target_values) def _scale(x, lb, ub): return (x - lb) / (ub - lb) def _to_unconstrained(x, lb, ub): x = _scale(x, lb, ub) return -tf.math.log((1.0 - x) / x) def _to_constrained(x, lb, ub): x = tf.math.exp(x) / (1.0 + tf.math.exp(x)) return x * (ub - lb) + lb if calibrate_correlation: num_thetas = num_hjm_factors * (num_hjm_factors - 1) init_corr = tf.range(0.1, num_thetas + 0.1, dtype=dtype) / num_thetas else: init_corr = [] if mean_reversion.shape.rank > 1: init_corr = [[]] * mean_reversion.shape.rank initial_guess = tf.concat([ _to_unconstrained(mean_reversion, mr_lb, mr_ub), _to_unconstrained(volatility, vol_lb, vol_ub), _to_unconstrained(init_corr, theta_lb, theta_ub) ], axis=-1) 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_hjm_factors], mr_lb, mr_ub) x_vol = _to_constrained(x[..., num_hjm_factors:2 * num_hjm_factors], vol_lb, vol_ub) if calibrate_correlation: thetas = x[..., 2 * num_hjm_factors:] thetas = tfp.math.clip_by_value_preserve_gradient(thetas, -25.0, 25.0) x_corr = _correlation_matrix_using_hypersphere_decomposition( num_hjm_factors, _to_constrained(thetas, theta_lb, theta_ub)) else: x_corr = None volatility_param = _make_hjm_volatility_fn(x_vol, dtype) # TODO(b/182663434): Use precomputed random draws. model_values = swaption_price( expiries=expiries, fixed_leg_payment_times=fixed_leg_payment_times, fixed_leg_daycount_fractions=fixed_leg_daycount_fractions, fixed_leg_coupon=fixed_leg_coupon, reference_rate_fn=reference_rate_fn, num_hjm_factors=num_hjm_factors, mean_reversion=x_mr, volatility=volatility_param, corr_matrix=x_corr, notional=notional, is_payer_swaption=is_payer_swaption, valuation_method=swaption_valuation_method, num_samples=num_samples, random_type=random_type, seed=seed, skip=skip, times=times, time_step=time_step, num_time_steps=num_time_steps, curve_times=curve_times, time_step_finite_difference=time_step_finite_difference, num_grid_points_finite_difference=num_grid_points_finite_difference, dtype=dtype) if volatility_based_calibration: model_values = _price_to_normal_vol(model_values, swap_rate, annuity) model_values = tf.where( tf.math.is_nan(model_values), tf.constant(1e-7, dtype=dtype), model_values) value = tf.math.reduce_sum( (_scale(model_values, target_lb, target_ub) - scaled_target)**2, axis=-1) 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 = _to_constrained( calibrated_parameters[..., :num_hjm_factors], mr_lb, mr_ub) volatility_calibrated = _to_constrained( calibrated_parameters[..., num_hjm_factors:2 * num_hjm_factors], vol_lb, vol_ub) if calibrate_correlation: correlation_calibrated = ( _correlation_matrix_using_hypersphere_decomposition( num_hjm_factors, _to_constrained( calibrated_parameters[..., 2 * num_hjm_factors:], theta_lb, theta_ub))) else: correlation_calibrated = None return (CalibrationResult(mean_reversion=mean_reversion_calibrated, volatility=volatility_calibrated, corr_matrix=correlation_calibrated), optimization_result.converged, optimization_result.num_iterations)
def discount_factors_and_bond_prices_from_samples( expiries, payment_times, sample_discount_curve_paths_fn, num_samples, time_step, dtype=None): """Utility function to compute the discount factors and the bond prices. Args: expiries: A real `Tensor` of any and dtype. The time to expiration of the swaptions. The shape of this input determines the number (and shape) of swaptions to be priced and the shape of the output - e.g. if there are two swaptions, and there are 11 payment dates for each swaption, then the shape of `expiries` is [2, 11], with entries repeated along the second axis. payment_times: A real `Tensor` of same dtype and compatible shape with `expiries` - e.g. if there are two swaptions, and there are 11 payment dates for each swaption, then the shape of `payment_times` should be [2, 11] sample_discount_curve_paths_fn: Callable which takes the following args: 1) times: Rank 1 `Tensor` of positive real values, specifying the times at which the path points are to be evaluated. 2) curve_times: Rank 1 `Tensor` of positive real values, specifying the maturities at which the discount curve is to be computed at each simulation time. 3) num_samples: Positive scalar integer specifying the number of paths to draw. Returns two `Tensor`s, the first being a Rank-4 tensor of shape [num_samples, m, k, d] containing the simulated zero coupon bond curves, and the second being a `Tensor` of shape [num_samples, k, d] containing the simulated short rate paths. Here, m is the size of `curve_times`, k is the size of `times`, and d is the dimensionality of the paths. num_samples: Positive scalar `int32` `Tensor`. The number of simulation paths during Monte-Carlo valuation. time_step: Scalar real `Tensor`. Maximal distance between time grid points in Euler scheme. Relevant when Euler scheme is used for simulation. 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. Returns: Two real tensors, `discount_factors` and `bond_prices`, both of shape [num_samples] + shape(payment_times) + [dim], where `dim` is the dimension of each path (e.g for a Hull-White with two models, dim==2; while for HJM dim==1 always.) """ sim_times, _ = tf.unique(tf.reshape(expiries, shape=[-1])) longest_expiry = tf.reduce_max(sim_times) sim_times, _ = tf.unique( tf.concat([sim_times, tf.range(time_step, longest_expiry, time_step)], axis=0)) sim_times = tf.sort(sim_times, name='sort_sim_times') swaptionlet_shape = payment_times.shape tau = payment_times - expiries curve_times_builder, _ = tf.unique(tf.reshape(tau, shape=[-1])) curve_times = tf.sort(curve_times_builder, name='sort_curve_times') p_t_tau, r_t = sample_discount_curve_paths_fn(times=sim_times, curve_times=curve_times, num_samples=num_samples) dim = p_t_tau.shape[-1] dt = tf.concat(axis=0, values=[ tf.convert_to_tensor([0.0], dtype=dtype), sim_times[1:] - sim_times[:-1] ]) dt = tf.expand_dims(tf.expand_dims(dt, axis=-1), axis=0) # Compute the discount factors. We do this by performing the following: # # 1. We compute the implied discount factors. These are the factors: # P(t1) = exp(-r1 * t1), # P(t1, t2) = exp(-r2 (t2 - t1)) # P(t2, t3) = exp(-r3 (t3 - t2)) # ... # 2. We compute the cumulative products to get P(t2), P(t3), etc.: # P(t2) = P(t1) * P(t1, t2) # P(t3) = P(t1) * P(t1, t2) * P(t2, t3) # ... # We perform the cumulative product by taking the cumulative sum over # log P's, and then exponentiating the sum. However, since each P is itself # an exponential, this effectively amounts to taking a cumsum over the # exponents themselves, and exponentiating in the end: # # P(t1) = exp(-r1 * t1) # P(t2) = exp(-r1 * t1 - r2 * (t2 - t1)) # P(t3) = exp(-r1 * t1 - r2 * (t2 - t1) - r3 * (t3 - t2)) # P(tk) = exp(-r1 * t1 - r2 * (t2 - t1) ... - r_k * (t_k - t_k-1)) # Transpose before (and after) because we want the cumprod along axis=1 # but `cumsum_using_matvec` operates on the last axis. cumul_rdt = tf.transpose(utils.cumsum_using_matvec( tf.transpose(r_t * dt, perm=[0, 2, 1])), perm=[0, 2, 1]) discount_factors = tf.math.exp(-cumul_rdt) # Make discount factors the same shape as `p_t_tau`. This involves adding # an extra dimenstion (corresponding to `curve_times`). discount_factors = tf.expand_dims(discount_factors, axis=1) # tf.repeat is needed because we will use gather_nd later on this tensor. discount_factors_simulated = tf.repeat(discount_factors, tf.shape(p_t_tau)[1], axis=1) # `sim_times` and `curve_times` are sorted for simulation. We need to # select the indices corresponding to our input. sim_time_index = tf.searchsorted(sim_times, tf.reshape(expiries, [-1])) curve_time_index = tf.searchsorted(curve_times, tf.reshape(tau, [-1])) gather_index = _prepare_indices_ijjk(tf.range(0, num_samples), curve_time_index, sim_time_index, tf.range(0, dim)) # The shape after `gather_nd` will be `(num_samples*num_swaptionlets*dim,)` payoff_discount_factors_builder = tf.gather_nd(discount_factors_simulated, gather_index) # Reshape to `[num_samples] + swaptionlet.shape + [dim]` payoff_discount_factors = tf.reshape(payoff_discount_factors_builder, [num_samples] + swaptionlet_shape + [dim]) payoff_bond_price_builder = tf.gather_nd(p_t_tau, gather_index) payoff_bond_price = tf.reshape(payoff_bond_price_builder, [num_samples] + swaptionlet_shape + [dim]) return payoff_discount_factors, payoff_bond_price
def bs_lsm_price(spots: types.FloatTensor, expiry_times: types.FloatTensor, strikes: types.FloatTensor, volatility: types.FloatTensor, discount_factors: types.FloatTensor, num_samples: int = 100000, num_exercise_times: int = 100, basis_fn=None, seed: Tuple[int, int] = (1, 2), is_call_option: types.BoolTensor = True, num_calibration_samples: int = None, dtype: types.Dtype = None, name: str = None): """Computes American option price via LSM under Black-Scholes model. Args: spots: A rank 1 real `Tensor` with spot prices. expiry_times: A `Tensor` of the same shape and dtype as `spots` representing expiry values of the options. strikes: A `Tensor` of the same shape and dtype as `spots` representing strike price of the options. volatility: A `Tensor` of the same shape and dtype as `spots` representing volatility values of the options. discount_factors: A `Tensor` of the same shape and dtype as `spots` representing discount factors at the expiry times. num_samples: Number of Monte Carlo samples. num_exercise_times: Number of excercise times for American options. basis_fn: Callable from a `Tensor` of the same shape `[num_samples, num_exercice_times, 1]` (corresponding to Monte Carlo samples) and a positive integer `Tenor` (representing a current time index) to a `Tensor` of shape `[basis_size, num_samples]` of the same dtype as `spots`. The result being the design matrix used in regression of the continuation value of options. This is the same argument as in `lsm_algorithm.least_square_mc`. seed: A tuple of 2 integers setting global and local seed of the Monte Carlo sampler is_call_option: A bool `Tensor`. num_calibration_samples: An optional integer less or equal to `num_samples`. The number of sampled trajectories used for the LSM regression step. Default value: `None`, which means that all samples are used for regression. dtype: `tf.Dtype` of the input and output real `Tensor`s. Default value: `None` which maps to `float64`. name: Python str. The name to give to the ops created by this class. Default value: `None` which maps to 'forward_rate_agreement'. Returns: A `Tensor` of the same shape and dtyoe as `spots` representing american option prices. """ dtype = dtype or tf.float64 name = name or "bs_lsm_price" with tf.name_scope(name): strikes = tf.convert_to_tensor(strikes, dtype=dtype, name="strikes") spots = tf.convert_to_tensor(spots, dtype=dtype, name="spots") volatility = tf.convert_to_tensor(volatility, dtype=dtype, name="volatility") expiry_times = tf.convert_to_tensor(expiry_times, dtype=dtype, name="expiry_times") discount_factors = tf.convert_to_tensor(discount_factors, dtype=dtype, name="discount_factors") risk_free_rate = -tf.math.log(discount_factors) / expiry_times # Normalize expiry times var = volatility**2 expiry_times = expiry_times * var gbm = models.GeometricBrownianMotion(mu=0.0, sigma=1.0, dtype=dtype) max_time = tf.reduce_max(expiry_times) # Get a grid of 100 exercise times + all expiry times unique_expiry = tf.unique(expiry_times).y times = tf.sort( tf.concat([ tf.linspace(tf.constant(0.0, dtype), max_time, num_exercise_times), unique_expiry ], axis=0)) # Samples for all options samples = gbm.sample_paths( times, initial_state=1.0, num_samples=num_samples, seed=seed, random_type=math.random.RandomType.STATELESS_ANTITHETIC) indices = tf.searchsorted(times, expiry_times) indices_ext = tf.expand_dims(indices, axis=-1) # Payoff function takes all the samples of shape # [num_paths, num_times, dim] and returns a `Tensor` of # shape [num_paths, num_strikes]. This corresponds to a # payoff at the present time. def _payoff_fn(sample_paths, time_index): current_samples = tf.transpose(sample_paths, [1, 2, 0])[time_index] r = tf.math.exp( tf.expand_dims(risk_free_rate / var, axis=-1) * times[time_index]) s = tf.expand_dims(spots, axis=-1) call_put = tf.expand_dims(is_call_option, axis=-1) payoff = tf.expand_dims(strikes, -1) - r * s * current_samples payoff = tf.where(call_put, tf.nn.relu(-payoff), tf.nn.relu(payoff)) # Since the pricing is happening on the grid, # For options, which have already expired, the payoff is set to `0` # to indicate that one should not exercise the option after it has expired res = tf.where(time_index > indices_ext, tf.constant(0, dtype=dtype), payoff) return tf.transpose(res) if basis_fn is None: # Polynomial basis with 2 functions basis_fn = lsm_algorithm.make_polynomial_basis_v2(2) # Set up Longstaff-Schwartz algorithm def lsm_price(sample_paths): num_times = int(times.shape[0]) # This is Longstaff-Schwartz algorithm return lsm_algorithm.least_square_mc_v2( sample_paths=sample_paths, exercise_times=list(range(num_times)), payoff_fn=_payoff_fn, basis_fn=basis_fn, discount_factors=tf.math.exp( -tf.reshape(risk_free_rate / var, [1, -1, 1]) * times), num_calibration_samples=num_calibration_samples) return lsm_price(samples)
def discount_factors_and_bond_prices_from_samples( expiries: types.RealTensor, payment_times: types.RealTensor, sample_discount_curve_paths_fn: Callable[..., Tuple[types.RealTensor, types.RealTensor, types.RealTensor]], num_samples: types.IntTensor, times: types.RealTensor = None, curve_times: types.RealTensor = None, dtype: tf.DType = None) -> Tuple[types.RealTensor, types.RealTensor]: """Utility function to compute the discount factors and the bond prices. Args: expiries: A real `Tensor` of any shape and dtype. The time to expiration of the swaptions. The shape of this input determines the number (and shape) of swaptions to be priced and the shape of the output - e.g. if there are two swaptions, and there are 11 payment dates for each swaption, then the shape of `expiries` is [2, 11], with entries repeated along the second axis. payment_times: A real `Tensor` of same dtype and compatible shape with `expiries` - e.g. if there are two swaptions, and there are 11 payment dates for each swaption, then the shape of `payment_times` should be [2, 11] sample_discount_curve_paths_fn: Callable which takes the following args: 1) times: Rank 1 `Tensor` of positive real values, specifying the times at which the path points are to be evaluated. 2) curve_times: Rank 1 `Tensor` of positive real values, specifying the maturities at which the discount curve is to be computed at each simulation time. 3) num_samples: Positive scalar integer specifying the number of paths to draw. Returns three `Tensor`s, the first being a N-D tensor of shape `model_batch_shape + [num_samples, m, k, d]` containing the simulated zero coupon bond curves, the second being a `Tensor` of shape `model_batch_shape + [num_samples, k, d]` containing the simulated short rate paths, the third `Tensor` of shape `model_batch_shape + [num_samples, k, d]` containing the simulated path discount factors. Here, m is the size of `curve_times`, k is the size of `times`, d is the dimensionality of the paths and `model_batch_shape` is shape of the batch of independent HJM models. num_samples: Positive scalar `int32` `Tensor`. The number of simulation paths during Monte-Carlo valuation. times: An optional rank 1 `Tensor` of increasing positive real values. The times at which Monte Carlo simulations are performed. Default value: `None`. curve_times: An optional rank 1 `Tensor` of positive real values. The maturities at which spot discount curve is computed during simulations. Default value: `None`. 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. Returns: Two real tensors, `discount_factors` and `bond_prices`, both of shape [num_samples] + swaption_batch_shape + [dim], where `dim` is the dimension of each path (e.g for a Hull-White with two models, dim==2; while for HJM dim==1 always). `swaption_batch_shape` has the same rank as `expiries.shape` and its leading dimensions are broadcasted to `model_batch_shape`. """ if times is not None: sim_times = tf.convert_to_tensor(times, dtype=dtype) else: # This might not be the most efficient if we have a batch of Models each # pricing swaptions with different expiries. sim_times = tf.reshape(expiries, shape=[-1]) sim_times = tf.sort(sim_times, name='sort_sim_times') swaptionlet_shape = tf.shape(payment_times) tau = payment_times - expiries if curve_times is not None: curve_times = tf.convert_to_tensor(curve_times, dtype=dtype) else: # This might not be the most efficient if we have a batch of Models each # pricing swaptions with different expiries and payment times. curve_times = tf.reshape(tau, shape=[-1]) curve_times, _ = tf.unique(curve_times) curve_times = tf.sort(curve_times, name='sort_curve_times') p_t_tau, r_t, discount_factors = sample_discount_curve_paths_fn( times=sim_times, curve_times=curve_times, num_samples=num_samples) dim = tf.shape(p_t_tau)[-1] model_batch_shape = tf.shape(p_t_tau)[:-4] model_batch_rank = p_t_tau.shape[:-4].rank instr_batch_shape = tf.shape(expiries)[model_batch_rank:] try: swaptionlet_shape = tf.concat( [model_batch_shape, instr_batch_shape], axis=0) expiries = tf.broadcast_to(expiries, swaptionlet_shape) tau = tf.broadcast_to(tau, swaptionlet_shape) except: raise ValueError('The leading dimensions of `expiries` of shape {} are not ' 'compatible with the batch shape {} of the model.'.format( expiries.shape.as_list(), p_t_tau.shape.as_list()[:-4])) if discount_factors is None: dt = tf.concat(axis=0, values=[[0.0], sim_times[1:] - sim_times[:-1]]) dt = tf.expand_dims(tf.expand_dims(dt, axis=-1), axis=0) # Transpose before (and after) because we want the cumprod along axis=1 # but `cumsum_using_matvec` operates on the last axis. Also we use cumsum # and then exponentiate instead of taking cumprod of exponents for # efficiency. cumul_rdt = tf.transpose( utils.cumsum_using_matvec(tf.transpose(r_t * dt, perm=[0, 2, 1])), perm=[0, 2, 1]) discount_factors = tf.math.exp(-cumul_rdt) # Make discount factors the same shape as `p_t_tau`. This involves adding # an extra dimension (corresponding to `curve_times`). discount_factors = tf.expand_dims(discount_factors, axis=model_batch_rank + 1) # tf.repeat is needed because we will use gather_nd later on this tensor. discount_factors_simulated = tf.repeat( discount_factors, tf.shape(p_t_tau)[model_batch_rank + 1], axis=model_batch_rank + 1) # `sim_times` and `curve_times` are sorted for simulation. We need to # select the indices corresponding to our input. new_shape = tf.concat([model_batch_shape, [-1]], axis=0) sim_time_index = tf.searchsorted(sim_times, tf.reshape(expiries, [-1])) curve_time_index = tf.searchsorted(curve_times, tf.reshape(tau, [-1])) sim_time_index = tf.reshape(sim_time_index, new_shape) curve_time_index = tf.reshape(curve_time_index, new_shape) gather_index = tf.stack([curve_time_index, sim_time_index], axis=-1) # shape=[num_samples] + batch_shape + [len(sim_times_index), dim] discount_factors_simulated = _gather_tensor_at_swaption_payoff( discount_factors_simulated, gather_index) payoff_discount_factors = tf.reshape( discount_factors_simulated, tf.concat([[num_samples], swaptionlet_shape, [dim]], axis=0)) # shape=[num_samples, len(sim_times_index), dim] p_t_tau = _gather_tensor_at_swaption_payoff(p_t_tau, gather_index) payoff_bond_price = tf.reshape( p_t_tau, tf.concat([[num_samples], swaptionlet_shape, [dim]], axis=0)) return payoff_discount_factors, payoff_bond_price
def options_price_from_samples(strikes, expiries, maturities, is_call_options, sample_discount_curve_paths_fn, num_samples, time_step, dtype=None): """Computes the zero coupon bond options price from simulated discount curves. Args: strikes: A real `Tensor` of any shape and dtype. The strike price of the options. The shape of this input determines the number (and shape) of the options to be priced and the output. expiries: A real `Tensor` of the same dtype and compatible shape as `strikes`. The time to expiry of each bond option. maturities: A real `Tensor` of the same dtype and compatible shape as `strikes`. The time to maturity of the underlying zero coupon bonds. is_call_options: A boolean `Tensor` of a shape compatible with `strikes`. Indicates whether the option is a call (if True) or a put (if False). sample_discount_curve_paths_fn: Callable which takes the following args: 1) times: Rank 1 `Tensor` of positive real values, specifying the times at which the path points are to be evaluated. 2) curve_times: Rank 1 `Tensor` of positive real values, specifying the maturities at which the discount curve is to be computed at each simulation time. 3) num_samples: Positive scalar integer specifying the number of paths to draw. and returns two `Tensor`s, the first being a Rank-4 tensor of shape [num_samples, m, k, d] containing the simulated zero coupon bond curves, and the second being a `Tensor` of shape [num_samples, k, d] containing the simulated short rate paths. Here, m is the size of `curve_times`, k is the size of `times`, and d is the dimensionality of the paths. num_samples: Positive scalar `int32` `Tensor`. The number of simulation paths during Monte-Carlo valuation. time_step: Scalar real `Tensor`. Maximal distance between time grid points in Euler scheme. Relevant when Euler scheme is used for simulation. 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. Returns: A `Tensor` of real dtype and shape `strikes.shape + [dim]` containing the computed option prices. """ sim_times, _ = tf.unique(tf.reshape(expiries, shape=[-1])) longest_expiry = tf.reduce_max(sim_times) sim_times, _ = tf.unique( tf.concat( [sim_times, tf.range(time_step, longest_expiry, time_step)], axis=0)) sim_times = tf.sort(sim_times, name='sort_sim_times') tau = maturities - expiries curve_times_builder, _ = tf.unique(tf.reshape(tau, shape=[-1])) curve_times = tf.sort(curve_times_builder, name='sort_curve_times') p_t_tau, r_t = sample_discount_curve_paths_fn( times=sim_times, curve_times=curve_times, num_samples=num_samples) dim = p_t_tau.shape[-1] dt_builder = tf.concat( axis=0, values=[ tf.convert_to_tensor([0.0], dtype=dtype), sim_times[1:] - sim_times[:-1] ]) dt = tf.expand_dims(tf.expand_dims(dt_builder, axis=-1), axis=0) discount_factors_builder = tf.math.exp(-r_t * dt) # Transpose before (and after) because we want the cumprod along axis=1 # and `matvec` operates on the last axis. The shape before and after would # be `(num_samples, len(times), dim)` discount_factors_builder = tf.transpose( utils.cumprod_using_matvec( tf.transpose(discount_factors_builder, [0, 2, 1])), [0, 2, 1]) # make discount factors the same shape as `p_t_tau`. This involves adding # an extra dimenstion (corresponding to `curve_times`). discount_factors_builder = tf.expand_dims(discount_factors_builder, axis=1) discount_factors_simulated = tf.repeat( discount_factors_builder, p_t_tau.shape.as_list()[1], axis=1) # `sim_times` and `curve_times` are sorted for simulation. We need to # select the indices corresponding to our input. sim_time_index = tf.searchsorted(sim_times, tf.reshape(expiries, [-1])) curve_time_index = tf.searchsorted(curve_times, tf.reshape(tau, [-1])) gather_index = _prepare_indices( tf.range(0, num_samples), curve_time_index, sim_time_index, tf.range(0, dim)) # The shape after `gather_nd` would be (num_samples*num_strikes*dim,) payoff_discount_factors_builder = tf.gather_nd(discount_factors_simulated, gather_index) # Reshape to `[num_samples] + strikes.shape + [dim]` payoff_discount_factors = tf.reshape(payoff_discount_factors_builder, [num_samples] + strikes.shape + [dim]) payoff_bond_price_builder = tf.gather_nd(p_t_tau, gather_index) payoff_bond_price = tf.reshape(payoff_bond_price_builder, [num_samples] + strikes.shape + [dim]) is_call_options = tf.reshape( tf.broadcast_to(is_call_options, strikes.shape), [1] + strikes.shape + [1]) strikes = tf.reshape(strikes, [1] + strikes.shape + [1]) payoff = tf.where(is_call_options, tf.math.maximum(payoff_bond_price - strikes, 0.0), tf.math.maximum(strikes - payoff_bond_price, 0.0)) option_value = tf.math.reduce_mean(payoff_discount_factors * payoff, axis=0) return option_value