Example #1
0
def _maybe_validate_perm(initial_rightmost_transposed_ndims,
                         perm,
                         validate_args,
                         name=None):
    """Checks that `perm` is valid."""
    with tf.name_scope(name or 'maybe_validate_perm'):
        assertions = []
        if not dtype_util.is_integer(perm.dtype):
            raise TypeError('`perm` must be integer type')

        msg = '`perm` must be a vector.'
        if tensorshape_util.rank(perm.shape) is not None:
            if tensorshape_util.rank(perm.shape) != 1:
                raise ValueError(msg[:-1] + ', saw rank: {}.'.format(
                    tensorshape_util.rank(perm.shape)))
        elif validate_args:
            assertions += [
                assert_util.assert_rank(perm, 1, message=msg),
                assert_util.assert_equal(
                    tf.size(perm),
                    initial_rightmost_transposed_ndims,
                    message='The number of elements of `perm` must not '
                    'change from the value set when the `Transpose` '
                    'bijector was constructed.')
            ]

        perm_ = tf.get_static_value(perm)
        msg = '`perm` must be a valid permutation vector.'
        if perm_ is not None:
            if not np.all(np.arange(np.size(perm_)) == np.sort(perm_)):
                raise ValueError(msg[:-1] + ', saw: {}.'.format(perm_))
        elif validate_args:
            assertions += [
                assert_util.assert_equal(tf.sort(perm),
                                         tf.range(
                                             tf.size(perm,
                                                     out_type=perm.dtype)),
                                         message=msg)
            ]

        return assertions
Example #2
0
def get_shuffled_indices_and_labels(batch_size, num_samples, shuffle_fraction,
                                    num_steps):
  """Produce possibly shuffled indices and labels."""
  total_num_samples = batch_size * num_samples
  num_shuffled_examples = int(shuffle_fraction * total_num_samples)

  shuffle_labels = tf.random.shuffle(tf.cast(
      num_shuffled_examples*[1] +
      (total_num_samples - num_shuffled_examples) * [0], tf.int32))
  indices = tf.sort(random_choice_noreplace(
      total_num_samples, num_steps)[:, :5], axis=1)
  indices = randomly_reverse_indices(indices)
  shuffled_samples = tf.where(
      tf.less_equal(tf.random.uniform((total_num_samples, 1)), 0.5),
      tf.gather(indices, [1, 0, 3], axis=1),
      tf.gather(indices, [1, 4, 3], axis=1))
  ordered_samples = tf.gather(indices, [1, 2, 3], axis=1)
  indices = tf.where(tf.equal(tf.expand_dims(shuffle_labels, axis=-1), 1),
                     shuffled_samples, ordered_samples)

  return indices, shuffle_labels
Example #3
0
    def test_softquantile(self, quantile):
        # Builds the input vector so that the desired quantile always corresponds to
        # an exact integer index.
        num_points_before_quantile = 10
        step = quantile / num_points_before_quantile
        num_points = int(1.0 / step + 1.0)
        quantile_width = step

        axis = 1
        x = tf.random.uniform((3, num_points, 4), dtype=tf.float32)
        soft_q = ops.softquantiles(x,
                                   quantile,
                                   quantile_width,
                                   axis=axis,
                                   epsilon=1e-3)

        # Compare to getting the exact quantile.
        hard_q = tf.gather(tf.sort(x, direction='ASCENDING', axis=axis),
                           int(quantile * num_points),
                           axis=1)

        self.assertAllClose(soft_q, hard_q, 0.2, 0.2)
Example #4
0
def _grid_from_time_step(*, times, time_step, dtype, tolerance):
  """Creates a time grid from an input time step."""
  grid = tf.range(0.0, times[-1], time_step, dtype=dtype)
  all_times = tf.concat([times, grid], axis=0)
  all_times = tf.sort(all_times)

  # Remove duplicate points
  dt = all_times[1:] - all_times[:-1]
  dt = tf.concat([[1.0], dt], axis=-1)
  duplicate_mask = tf.math.greater(dt, tolerance)
  all_times = tf.boolean_mask(all_times, duplicate_mask)
  time_indices = tf.searchsorted(all_times, times, out_type=tf.int32)
  time_indices = tf.math.minimum(time_indices, tf.shape(all_times)[0] - 1)

  # Move `time_indices` to the left, if the requested `times` are removed from
  # `all_times` during deduplication
  time_indices = tf.where(
      tf.gather(all_times, time_indices) - times > tolerance,
      time_indices - 1,
      time_indices)

  return all_times, time_indices
Example #5
0
def trimmed_error(y_true, y_pred, start_quantile, end_quantile, power=1.0):
    """In regression, computes the mean of trimmed errors.

  Args:
   y_true: Tensor<float>[batch]
   y_pred: Tensor<float>[batch, 1] or Tensor<float>[batch].
   start_quantile: (float) errors below this quantile (as computed on all
    [batch] examples) are discarded. value in [0, 1].
   end_quantile: (float) errors above this quantile (as computed on all
    [batch] examples) are discarded. value in [0, 1].
   power: (float) an optional power on the absolute difference between y_pred
    and y_true.

  Returns:
    A Tensor<float>[1]: the mean error on the selected range of quantiles.
  """
    error = tf.pow(tf.abs(tf.squeeze(y_pred) - y_true), power)
    sorted_error = tf.sort(error, direction='ASCENDING', axis=0)
    n = tf.cast(tf.shape(y_true)[0], tf.float32)
    n_start = tf.cast(tf.math.floor(n * start_quantile), dtype=tf.int32)
    n_end = tf.cast(tf.math.floor(n * end_quantile), dtype=tf.int32)
    return tf.reduce_mean(sorted_error[n_start:n_end], axis=0)
Example #6
0
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
Example #7
0
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
    all_times = tf.sort(all_times)
    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
Example #8
0
  def _parameter_control_dependencies(self, is_init):
    assertions = []

    if is_init != tensor_util.is_ref(self.permutation):
      if not dtype_util.is_integer(self.permutation.dtype):
        raise TypeError('permutation.dtype ({}) should be `int`-like.'.format(
            dtype_util.name(self.permutation.dtype)))

      p = tf.get_static_value(self.permutation)
      if p is not None:
        if set(p) != set(np.arange(p.size)):
          raise ValueError('Permutation over `d` must contain exactly one of '
                           'each of `{0, 1, ..., d}`.')

      if self.validate_args:
        p = tf.sort(self.permutation, axis=-1)
        assertions.append(
            assert_util.assert_equal(
                p,
                tf.range(tf.shape(p)[-1]),
                message=('Permutation over `d` must contain exactly one of '
                         'each of `{0, 1, ..., d}`.')))

    return assertions
Example #9
0
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
        times = tf.sort(
            tf.concat([
                tf.linspace(tf.constant(0.0, dtype), max_time,
                            num_exercise_times), expiry_times
            ],
                      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):
            exercise_times = tf.range(tf.shape(times)[0])
            # This is Longstaff-Schwartz algorithm
            return lsm_algorithm.least_square_mc_v2(
                sample_paths=sample_paths,
                exercise_times=exercise_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)
Example #10
0
def percentile(x,
               q,
               axis=None,
               interpolation=None,
               keepdims=False,
               validate_args=False,
               preserve_gradients=True,
               keep_dims=None,
               name=None):
    """Compute the `q`-th percentile(s) of `x`.

  Given a vector `x`, the `q`-th percentile of `x` is the value `q / 100` of the
  way from the minimum to the maximum in a sorted copy of `x`.

  The values and distances of the two nearest neighbors as well as the
  `interpolation` parameter will determine the percentile if the normalized
  ranking does not match the location of `q` exactly.

  This function is the same as the median if `q = 50`, the same as the minimum
  if `q = 0` and the same as the maximum if `q = 100`.

  Multiple percentiles can be computed at once by using `1-D` vector `q`.
  Dimension zero of the returned `Tensor` will index the different percentiles.

  Compare to `numpy.percentile`.

  Args:
    x:  Numeric `N-D` `Tensor` with `N > 0`.  If `axis` is not `None`,
      `x` must have statically known number of dimensions.
    q:  Scalar or vector `Tensor` with values in `[0, 100]`. The percentile(s).
    axis:  Optional `0-D` or `1-D` integer `Tensor` with constant values. The
      axis that index independent samples over which to return the desired
      percentile.  If `None` (the default), treat every dimension as a sample
      dimension, returning a scalar.
    interpolation : {'nearest', 'linear', 'lower', 'higher', 'midpoint'}.
      Default value: 'nearest'.  This specifies the interpolation method to
      use when the desired quantile lies between two data points `i < j`:
        * linear: i + (j - i) * fraction, where fraction is the fractional part
          of the index surrounded by i and j.
        * lower: `i`.
        * higher: `j`.
        * nearest: `i` or `j`, whichever is nearest.
        * midpoint: (i + j) / 2.
      `linear` and `midpoint` interpolation do not work with integer dtypes.
    keepdims:  Python `bool`. If `True`, the last dimension is kept with size 1
      If `False`, the last dimension is removed from the output shape.
    validate_args:  Whether to add runtime checks of argument validity. If
      False, and arguments are incorrect, correct behavior is not guaranteed.
    preserve_gradients:  Python `bool`.  If `True`, ensure that gradient w.r.t
      the percentile `q` is preserved in the case of linear interpolation.
      If `False`, the gradient will be (incorrectly) zero when `q` corresponds
      to a point in `x`.
    keep_dims: deprecated, use keepdims instead.
    name:  A Python string name to give this `Op`.  Default is 'percentile'

  Returns:
    A `(rank(q) + N - len(axis))` dimensional `Tensor` of same dtype as `x`, or,
      if `axis` is `None`, a `rank(q)` `Tensor`.  The first `rank(q)` dimensions
      index quantiles for different values of `q`.

  Raises:
    ValueError:  If argument 'interpolation' is not an allowed type.
    ValueError:  If interpolation type not compatible with `dtype`.

  #### Examples

  ```python
  # Get 30th percentile with default ('nearest') interpolation.
  x = [1., 2., 3., 4.]
  tfp.stats.percentile(x, q=30.)
  ==> 2.0

  # Get 30th percentile with 'linear' interpolation.
  x = [1., 2., 3., 4.]
  tfp.stats.percentile(x, q=30., interpolation='linear')
  ==> 1.9

  # Get 30th and 70th percentiles with 'lower' interpolation
  x = [1., 2., 3., 4.]
  tfp.stats.percentile(x, q=[30., 70.], interpolation='lower')
  ==> [1., 3.]

  # Get 100th percentile (maximum).  By default, this is computed over every dim
  x = [[1., 2.]
       [3., 4.]]
  tfp.stats.percentile(x, q=100.)
  ==> 4.

  # Treat the leading dim as indexing samples, and find the 100th quantile (max)
  # over all such samples.
  x = [[1., 2.]
       [3., 4.]]
  tfp.stats.percentile(x, q=100., axis=[0])
  ==> [3., 4.]
  ```

  """
    keepdims = keepdims if keep_dims is None else keep_dims
    del keep_dims
    name = name or 'percentile'
    allowed_interpolations = {
        'linear', 'lower', 'higher', 'nearest', 'midpoint'
    }

    if interpolation is None:
        interpolation = 'nearest'
    else:
        if interpolation not in allowed_interpolations:
            raise ValueError(
                'Argument `interpolation` must be in {}. Found {}.'.format(
                    allowed_interpolations, interpolation))

    with tf.name_scope(name):
        x = tf.convert_to_tensor(x, name='x')

        if (interpolation in {'linear', 'midpoint'}
                and dtype_util.is_integer(x.dtype)):
            raise TypeError(
                '{} interpolation not allowed with dtype {}'.format(
                    interpolation, x.dtype))

        # Double is needed here and below, else we get the wrong index if the array
        # is huge along axis.
        q = tf.cast(q, tf.float64)
        _get_static_ndims(q, expect_ndims_no_more_than=1)

        if validate_args:
            q = distribution_util.with_dependencies([
                assert_util.assert_rank_in(q, [0, 1]),
                assert_util.assert_greater_equal(q, tf.cast(0., tf.float64)),
                assert_util.assert_less_equal(q, tf.cast(100., tf.float64))
            ], q)

        # Move `axis` dims of `x` to the rightmost, call it `y`.
        if axis is None:
            y = tf.reshape(x, [-1])
        else:
            x_ndims = _get_static_ndims(x,
                                        expect_static=True,
                                        expect_ndims_at_least=1)
            axis = _make_static_axis_non_negative_list(axis, x_ndims)
            y = _move_dims_to_flat_end(x, axis, x_ndims, right_end=True)

        frac_at_q_or_below = q / 100.

        # Sort (in ascending order) everything which allows multiple calls to sort
        # only once (under the hood) and use CSE.
        sorted_y = tf.sort(y, axis=-1, direction='ASCENDING')

        d = ps.cast(ps.shape(y)[-1], tf.float64)

        def _get_indices(interp_type):
            """Get values of y at the indices implied by interp_type."""
            if interp_type == 'lower':
                indices = tf.math.floor((d - 1) * frac_at_q_or_below)
            elif interp_type == 'higher':
                indices = tf.math.ceil((d - 1) * frac_at_q_or_below)
            elif interp_type == 'nearest':
                indices = tf.round((d - 1) * frac_at_q_or_below)
            # d - 1 will be distinct from d in int32, but not necessarily double.
            # So clip to avoid out of bounds errors.
            return tf.clip_by_value(tf.cast(indices, tf.int32), 0,
                                    ps.shape(y)[-1] - 1)

        if interpolation in ['nearest', 'lower', 'higher']:
            gathered_y = tf.gather(sorted_y,
                                   _get_indices(interpolation),
                                   axis=-1)
        elif interpolation == 'midpoint':
            gathered_y = 0.5 * (
                tf.gather(sorted_y, _get_indices('lower'), axis=-1) +
                tf.gather(sorted_y, _get_indices('higher'), axis=-1))
        elif interpolation == 'linear':
            # Copy-paste of docstring on interpolation:
            # linear: i + (j - i) * fraction, where fraction is the fractional part
            # of the index surrounded by i and j.
            larger_y_idx = _get_indices('higher')
            exact_idx = (d - 1) * frac_at_q_or_below
            if preserve_gradients:
                # If q corresponds to a point in x, we will initially have
                # larger_y_idx == smaller_y_idx.
                # This results in the gradient w.r.t. fraction being zero (recall `q`
                # enters only through `fraction`...and see that things cancel).
                # The fix is to ensure that smaller_y_idx and larger_y_idx are always
                # separated by exactly 1.
                smaller_y_idx = tf.maximum(larger_y_idx - 1, 0)
                larger_y_idx = tf.minimum(smaller_y_idx + 1,
                                          tf.shape(y)[-1] - 1)
                fraction = tf.cast(larger_y_idx, tf.float64) - exact_idx
            else:
                smaller_y_idx = _get_indices('lower')
                fraction = tf.math.ceil(
                    (d - 1) * frac_at_q_or_below) - exact_idx

            fraction = tf.cast(fraction, y.dtype)
            gathered_y = (
                tf.gather(sorted_y, larger_y_idx, axis=-1) * (1 - fraction) +
                tf.gather(sorted_y, smaller_y_idx, axis=-1) * fraction)

        # Propagate NaNs
        if x.dtype in (tf.bfloat16, tf.float16, tf.float32, tf.float64):
            # Apparently tf.is_nan doesn't like other dtypes
            nan_batch_members = tf.reduce_any(tf.math.is_nan(x), axis=axis)
            right_rank_matched_shape = ps.pad(ps.shape(nan_batch_members),
                                              paddings=[[0, ps.rank(q)]],
                                              constant_values=1)
            nan_batch_members = tf.reshape(nan_batch_members,
                                           shape=right_rank_matched_shape)
            nan = np.array(np.nan, dtype_util.as_numpy_dtype(gathered_y.dtype))
            gathered_y = tf.where(nan_batch_members, nan, gathered_y)

        # Expand dimensions if requested
        if keepdims:
            if axis is None:
                ones_vec = tf.ones(shape=[
                    _get_best_effort_ndims(x) + _get_best_effort_ndims(q)
                ],
                                   dtype=tf.int32)
                gathered_y *= tf.ones(ones_vec, dtype=x.dtype)
            else:
                gathered_y = _insert_back_keepdims(gathered_y, axis)

        # If q is a scalar, then result has the right shape.
        # If q is a vector, then result has trailing dim of shape q.shape, which
        # needs to be rotated to dim 0.
        return distribution_util.rotate_transpose(gathered_y, ps.rank(q))
Example #11
0
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
Example #12
0
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
Example #13
0
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
Example #14
0
def log_uniform_grid_with_extra_point(minimums,
                                      maximums,
                                      sizes,
                                      extra_grid_point,
                                      dtype=None,
                                      validate_args=False,
                                      name=None):
    """Creates a grid for a uniform grid in a log-space with an extra grid point.

  A log-uniform grid is characterized by having a constant gap between
  neighboring points along each axis in the log-space, i.e., the logarithm of
  output grid is the uniform grid.  An extra grid point is useful, for example,
  when computing sensitivities for a value through a grid pricing method.

  Note that the shape of all three parameters must be fully defined and equal
  to each other. The shape is used to determine the dimension of the grid.
  Note that all the parameters are supplied and returned for the original space
  and not the log-space.

  #### Examples

  ```python
  dtype = np.float64
  extra_locations = tf.constant([[0.5, 2], [2, 3]], dtype=dtype)
  min_x, max_x, sizes = [[0.1, 0.1], [0.01, 0.1]], [[10, 5], [100, 5]], [3, 2]
  # Here min_x and max_x are in the original space and *not* in the log-space.
  grid = log_uniform_grid_with_extra_point(
      min_x, max_x, sizes,
      extra_grid_point=extra_locations, dtype=dtype)
  with tf.Session() as sess:
    grid = sess.run(grid)
  # Note that the minimum and maximum grid locations are the same as min_x and
  # max_x.
  print(grid.locations[0])
  # [[0.1, 0.5, 1.0, 10.0], [0.01, 1.0, 2.0, 100.0]]
  print(grid.locations[1])
  # [[0.1, 2, 5], [0.1, 3, 5]]
  ```

  Args:
    minimums: Real `Tensor` of rank 1 or 2 containing the lower end points of
      the grid. Must have the same shape as those of `maximums`. When rank is 2
      the first dimension is the batch dimension.
    maximums: `Tensor` of the same dtype and shape as `minimums`. The upper
      endpoints of the grid.
    sizes: Integer rank 1 `Tensor` of the same shape as `minimums`. The size of
      the grid in each axis. Each entry must be greater than or equal to 2 (i.e.
      the sizes include the end points).
    extra_grid_point: A `Tensor` of the same `dtype` as `minimums` and of shape
      `[batch_size, n]`, where `batch_shape` is a positive integer and `n` is
      the number of points along a dimension. These are the extra points added
      to the grid, so that the output grid `locations` have shape `[batch_shape,
      n+1]`.
    dtype: Optional tf.dtype. The default dtype to use for the grid.
    validate_args: Python boolean indicating whether to validate the supplied
      arguments. The validation checks performed are (a) `maximums` > `minimums`
      (b) `minimums` > 0.0 (c) `sizes` >= 2.
    name: Python str. The name prefixed to the ops created by this function. If
      not supplied, the default name 'uniform_grid_spec' is used.

  Returns:
    The grid locations as projected along each axis. One `Tensor` of shape
    `[..., n]`, where `n` is the number of points along that axis. The first
    dimensions are the batch shape. The grid itself can be seen as a cartesian
    product of the locations array.

  Raises:
    ValueError if the shape of maximums, minimums and sizes are not fully
    defined or they are not identical to each other or they are not rank 1.
  """
    with tf.compat.v1.name_scope(name, 'log_uniform_grid',
                                 [minimums, maximums, sizes]):
        minimums = tf.convert_to_tensor(minimums, dtype=dtype, name='minimums')
        maximums = tf.convert_to_tensor(maximums, dtype=dtype, name='maximums')
        sizes = tf.convert_to_tensor(sizes, name='sizes')
        extra_grid_point = tf.convert_to_tensor(extra_grid_point,
                                                dtype=dtype,
                                                name='extra_grid_point')
        batch_shape = tf.shape(extra_grid_point)[0]
        # Check that the shape of `sizes` is statically defined.
        if not _check_shapes_fully_defined(minimums, maximums, sizes):
            raise ValueError('The shapes of minimums, maximums and sizes '
                             'must be fully defined.')

        if minimums.shape != maximums.shape:
            raise ValueError(
                'The shapes of minimums and maximums must be identical.')

        control_deps = []
        if validate_args:
            control_deps = [
                tf.compat.v1.debugging.assert_greater(maximums, minimums),
                tf.compat.v1.debugging.assert_greater(
                    minimums, tf.constant(0, dtype=dtype)),
                tf.compat.v1.debugging.assert_greater_equal(sizes, 2)
            ]
        # Generate a uniform grid in the log-space taking into account that the
        # arguments were already validated.
        locations = []
        with tf.compat.v1.control_dependencies(control_deps):
            dim = sizes.shape[0]
            log_maximums = tf.math.log(maximums)
            log_minimums = tf.math.log(minimums)
            for i in range(dim):
                locations.append(
                    tf.expand_dims(log_minimums[..., i], -1) + tf.expand_dims(
                        (log_maximums[..., i] - log_minimums[..., i]), -1) *
                    tf.linspace(tf.constant(0., dtype=minimums.dtype),
                                1.0,
                                num=sizes[i]))
            # Broadcast `locations` to the shape `[batch_shape, size]`
            for i, location in enumerate(locations):
                locations[i] = location + tf.zeros([batch_shape, sizes[i]],
                                                   dtype=dtype)
            # Add `extra_grid_point` to `locations`
            for i, location in enumerate(locations):
                location_update = tf.sort(
                    tf.concat([
                        location,
                        tf.expand_dims(tf.math.log(extra_grid_point[:, i]), -1)
                    ], -1), -1)
                locations[i] = tf.exp(location_update)
            return locations
Example #15
0
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
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 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 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 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
Example #20
0
            def _compute_bootstrap_lb_ub(reward_samples):
                """Compute Efron's bootstrap lb."""
                sample_mean = tf.reduce_mean(reward_samples)
                # Step 1, sample with replacement and compute subsampled mean
                uniform_log_prob = tf.tile(
                    tf.expand_dims(tf.zeros(num_episodes), 0),
                    [num_bootstraps, 1])
                ind = tf.random.categorical(uniform_log_prob,
                                            num_bootstrap_samples)
                bootstrap_subsamples = tf.gather(reward_samples, ind)
                subsample_means = tf.reduce_mean(bootstrap_subsamples, axis=1)

                # Step 2, sort subsample means, compute y, z_0, and a
                sorted_subsample_means = tf.sort(subsample_means,
                                                 axis=0,
                                                 direction='ASCENDING')

                # bias factor
                z_0 = gaussian_rv.quantile(
                    tf.reduce_sum(
                        tf.cast(
                            tf.greater(sample_mean, sorted_subsample_means),
                            tf.float32)) / float(num_bootstraps))
                # y is the leave-one-out, jackknife sample mean
                mask_matrix = tf.ones([num_episodes, num_episodes
                                       ]) - tf.eye(num_episodes)
                leave_one_out_subsample_sums = tf.einsum(
                    'j,jk->k', reward_samples, mask_matrix)
                ys = leave_one_out_subsample_sums / (num_episodes_float - 1)

                # average of jackknife estimate
                y_bar = tf.reduce_mean(ys)

                # acceleration factor
                d_ys = y_bar - ys
                a = tf.reduce_sum(tf.pow(d_ys, 3.0)) / tf.maximum(
                    eps, 6.0 * tf.pow(tf.reduce_sum(tf.pow(d_ys, 2.0)), 1.5))

                # Step 3, compute z_scores for lb and ub
                z_score_delta_tail = gaussian_rv.quantile(delta_tail_half)
                z_score_1_delta_tail = gaussian_rv.quantile(1.0 -
                                                            delta_tail_half)

                z_lb = z_0 + (z_score_delta_tail + z_0) / tf.maximum(
                    eps, 1 - a * (z_score_delta_tail + z_0))
                z_ub = z_0 + (z_score_1_delta_tail + z_0) / tf.maximum(
                    eps, 1 - a * (z_score_1_delta_tail + z_0))

                # Step 4, compute corresponding quantiles and get bootstrap intervals
                lb_index = tf.cast(
                    tf.maximum(
                        tf.minimum(
                            tf.floor(num_bootstraps * gaussian_rv.cdf(z_lb)),
                            num_bootstraps - 1), 1), tf.int64)
                ub_index = tf.cast(
                    tf.maximum(
                        tf.minimum(
                            tf.floor(num_bootstraps * gaussian_rv.cdf(z_ub)),
                            num_bootstraps - 1), 1), tf.int64)

                lb = tf.gather(sorted_subsample_means, lb_index)
                ub = tf.gather(sorted_subsample_means, ub_index)

                return lb, ub