def _adjust_convexity(self, valuation_date, market, model, pricing_context, cms_rates, discount_factors): """Computes the convexity adjusted cms rate.""" if model is None: return cms_rates elif model in ( rc.InterestRateModelType.LOGNORMAL_SMILE_CONSISTENT_REPLICATION, rc.InterestRateModelType.NORMAL_SMILE_CONSISTENT_REPLICATION): return self._convexity_smile_replication( valuation_date, market, model, cms_rates, pricing_context) else: level = self._swap.annuity(valuation_date, market, None) expiry_time = dates.daycount_actual_365_fixed( start_date=valuation_date, end_date=self._coupon_start_dates, dtype=self._dtype) with tf.GradientTape() as g: g.watch(cms_rates) fx = self._fs(cms_rates) dfx = tf.squeeze(g.gradient(fx, cms_rates)) swap_vol = tf.convert_to_tensor(pricing_context, dtype=self._dtype) if model == rc.InterestRateModelType.LOGNORMAL_RATE: cms_rates = cms_rates + dfx * level * (cms_rates**2) * ( tf.math.exp(swap_vol**2 * expiry_time) - 1.0) / discount_factors else: cms_rates = cms_rates + dfx * level * ( swap_vol**2 * expiry_time) / discount_factors return cms_rates
def _price_lognormal_rate(self, valuation_date, market, pricing_context): """Computes caplet/floorlet prices using lognormal model for forward rates. The function computes individual caplet prices for the batch of caps/floors using the lognormal model for the forward rates. If the volatilities are are supplied (through the input `pricing_context`) then they are used as forward rate volatilies. Otherwise, volatilities are extracted using the volatility surface for `market`. Args: valuation_date: A scalar `DateTensor` specifying the date on which valuation is being desired. market: A namedtuple of type `InterestRateMarket` which contains the necessary information for pricing the Cap/Floor. pricing_context: An optional input containing the black volatility for for the forward rates. Returns: A Rank 1 `Tensor` of real type containing the price of each caplet (or floorlet) based using the lognormal model for forward rates. """ discount_curve = market.discount_curve discount_factors = tf.where( self._payment_dates > valuation_date, discount_curve.get_discount_factor(self._payment_dates), 0.) forward_rates = self._get_forward_rate(valuation_date, market) if pricing_context is None: volatility_surface = market.volatility_curve black_vols = volatility_surface.interpolate( self._reset_dates, self._strike, self._term) else: black_vols = tf.convert_to_tensor(pricing_context, dtype=self._dtype) expiry_times = dates.daycount_actual_365_fixed( start_date=valuation_date, end_date=self._reset_dates, dtype=self._dtype) caplet_prices = black_scholes.option_price( forwards=forward_rates, strikes=self._strike, volatilities=black_vols, expiries=expiry_times, is_call_options=self._is_cap) intrinsic_value = tf.where( self._is_cap, tf.math.maximum(forward_rates - self._strike, 0.0), tf.math.maximum(self._strike - forward_rates, 0)) caplet_prices = tf.where( self._payment_dates < valuation_date, tf.constant(0., dtype=self._dtype), tf.where(self._accrual_start_dates < valuation_date, intrinsic_value, caplet_prices)) caplet_prices = self._notional * self._daycount_fractions * caplet_prices return discount_factors * caplet_prices
def get_daycount_fraction(date_start, date_end, convention, dtype): """Return the day count fraction between two dates.""" if convention == DayCountConvention.ACTUAL_365: return dates.daycount_actual_365_fixed( start_date=date_start, end_date=date_end, dtype=dtype) elif convention == DayCountConvention.ACTUAL_360: return dates.daycount_actual_360( start_date=date_start, end_date=date_end, dtype=dtype) elif convention == DayCountConvention.THIRTY_360_ISDA: return dates.daycount_thirty_360_isda( start_date=date_start, end_date=date_end, dtype=dtype) else: raise ValueError('Daycount convention not implemented.')
def price(self, valuation_date, market, model=None, pricing_context=None, name=None): """Returns the present value of the swaption on the valuation date. Args: valuation_date: A scalar `DateTensor` specifying the date on which valuation is being desired. market: A namedtuple of type `InterestRateMarket` which contains the necessary information for pricing the FRA instrument. model: An optional input of type `InterestRateModelType` to specify which model to use for pricing. Default value: `None` in which case LOGNORMAL_RATE model is used. pricing_context: An optional input to provide additional parameters (such as model parameters) relevant for pricing. name: Python str. The name to give to the ops created by this function. Default value: `None` which maps to 'price'. Returns: A Rank 1 `Tensor` of real type containing the modeled price of each IRS contract based on the input market data. Raises: ValueError: If an unsupported model is supplied to the function. """ model = model or rc.InterestRateModelType.LOGNORMAL_RATE name = name or (self._name + '_price') with tf.name_scope(name): swap_annuity = self._swap.annuity(valuation_date, market, model) forward_swap_rate = self._swap.par_rate(valuation_date, market, model) strike = self._swap.fixed_rate expiry_time = dates.daycount_actual_365_fixed( start_date=valuation_date, end_date=self._expiry_date, dtype=self._dtype) # Ideally we would like the model to tell us how to price the option. # The default for European swaptions should be SABR, but the current # implementation needs some work. if model == rc.InterestRateModelType.LOGNORMAL_RATE: option_value = self._price_lognormal_rate( market, pricing_context, forward_swap_rate, strike, expiry_time) else: raise ValueError('Unsupported model.') return self._swap.notional[-1] * swap_annuity * option_value
def price(self, market: pmd.ProcessedMarketData, name: Optional[str] = None): """Returns the present value of the swaption on the valuation date. Args: market: A instance of type `ProcessedMarketData` which contains the necessary information for pricing the swaption. name: Python str. The name to give to the ops created by this function. Default value: `None` which maps to 'price'. Returns: A Rank `Tensor` of shape `batch_shape` containing the modeled price of each Swaption contract based on the input market data. Raises: ValueError: If an unsupported model is supplied to the function. """ model = (self._config.model or models.InterestRateModelType.HULL_WHITE_ONE_FACTOR) name = name or (self._name + "_price") with tf.name_scope(name): valuation_date = dateslib.convert_to_date_tensor(market.date) strike = self._swap.fixed_rate() expiry_time = dateslib.daycount_actual_365_fixed( start_date=valuation_date, end_date=self._expiry_date, dtype=self._dtype) if model == models.InterestRateModelType.HULL_WHITE_ONE_FACTOR: option_value = self._price_hull_white_1_factor( valuation_date, market, strike, expiry_time) else: raise ValueError("Unsupported model.") return option_value
def get_forward_rate(self, start_date, maturity_date, daycount_fraction=None): """Returns the simply accrued forward rate between [start_dt, maturity_dt]. Args: start_date: A `DateTensor` specifying the start of the accrual period for the forward rate. maturity_date: A `DateTensor` specifying the end of the accrual period for the forward rate. The shape of `maturity_date` must be the same as the shape of the `DateTensor` `start_date`. daycount_fraction: An optional `Tensor` of real dtype specifying the time between `start_date` and `maturity_date` in years computed using the forward rate's day count basis. The shape of the input should be the same as that of `start_date` and `maturity_date`. Default value: `None`, in which case the daycount fraction is computed using `ACTUAL_365` convention. Returns: A real tensor of same shape as the inputs containing the simply compounded forward rate. """ start_date = dates.convert_to_date_tensor(start_date) maturity_date = dates.convert_to_date_tensor(maturity_date) if daycount_fraction is None: daycount_fraction = dates.daycount_actual_365_fixed( start_date=start_date, end_date=maturity_date, dtype=self._dtype) else: daycount_fraction = tf.convert_to_tensor(daycount_fraction, self._dtype) dfstart = self.get_discount_factor(start_date) dfmaturity = self.get_discount_factor(maturity_date) return (dfstart / dfmaturity - 1.) / daycount_fraction
def _get_time(self, desired_dates): """Computes the year fraction from the curve's valuation date.""" return dates.daycount_actual_365_fixed(start_date=self._valuation_date, end_date=desired_dates, dtype=self._dtype)
def from_market_data(cls, valuation_date, expiry_dates, strikes, implied_volatilities, variance_process, initial_spot, initial_variance, rho=None, risk_free_rate=None, dividend_yield=None, time_step=None, num_grid_points=None, grid_minimums=None, grid_maximums=None, dtype=None): """Creates a `LocalStochasticVolatilityModel` from market data. This function computes the leverage function for the LSV model by first computing the joint probability density function `p(t, X(t), v(t))` where `X(t)` is the log of the spot price and `v(t)` is the variance at time `t`. The joint probablity density is computed using the Fokker-Planck equation of the LSV model (see 6.8.2 in Ref [1]): ```None dp/dt = 1/2 d^2 [v L(t,X)^2 p]/dX^2 + 1/2 d^2 [b(v)^2 p]/dv^2 + rho d^2 [sqrt(v)L(t,X)b(v) p]/dXdv - d[(r - d - 1/2 v L(t,X)^2)p]/dX - d[a(v) p]/dv ``` where `a(v)` and `b(v)` are the drift and diffusion functions for the variance process. Defining ```None I_n(k,t) = int v^n p(t, k, v) dv ``` we can calculate the leverage function as follows: ```None L(k, t) = sigma(exp(k), t) sqrt(I_0(k, t)/I_1(k, t)). ``` Note that the computation of `I_0` and `I_1` require the knowledge of leverage function and hence the computation of the leverage function is implicit in nature. Args: valuation_date: A scalar `DateTensor` specifying the valuation (or settlement) date for the market data. expiry_dates: A `DateTensor` of shape `(num_expiries,)` containing the expiry dates on which the implied volatilities are specified. strikes: A `Tensor` of real dtype and shape `(num_expiries, num_strikes)` specifying the strike prices at which implied volatilities are specified. implied_volatilities: A `Tensor` of real dtype and shape `(num_expiries, num_strikes)` specifying the implied volatilities. variance_process: An instance of `LSVVarianceModel` or `ItoProcess` specifying the dynamics of the variance process of the LSV model. initial_spot: A real scalar `Tensor` specifying the underlying spot price on the valuation date. initial_variance: A real scalar `Tensor` specifying the initial variance on the valuation date. rho: A real scalar `Tensor` specifying the correlation between spot price and the stochastic variance. risk_free_rate: A real scalar `Tensor` specifying the (continuosly compounded) risk free interest rate. If the underlying is an FX rate, then use this input to specify the domestic interest rate. Default value: `None` in which case the input is set to zero. dividend_yield: A real scalar `Tensor` specifying the (continuosly compounded) divident yield. If the underlying is an FX rate, then use this input to specify the foreign interest rate. Default value: `None` in which case the input is set to zero. time_step: A real scalar `Tensor` specifying the time step during the numerical solution of the Fokker-Planck PDE. Default value: None, in which case `time_step` corresponding to 100 time steps is used. num_grid_points: A scalar integer `Tensor` specifying the number of discretization points for each spatial dimension. Default value: None, in which case number of grid points is set to 100. grid_minimums: An optional `Tensor` of size 2 containing the minimum grid points for PDE spatial discretization. `grid_minimums[0]` correspond to the minimum spot price in the spatial grid and `grid_minimums[1]` correspond to the minimum variance value. grid_maximums: An optional `Tensor` of size 2 containing the maximum grid points for PDE spatial discretization. `grid_maximums[0]` correspond to the maximum spot price in the spatial grid and `grid_maximums[1]` correspond to the maximum variance value. 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: An instance of `LocalStochasticVolatilityModel` constructed using the input data. """ if risk_free_rate is None: discount_factor_fn = lambda t: tf.ones_like(t, dtype=dtype) else: r = tf.convert_to_tensor(risk_free_rate, dtype=dtype) discount_factor_fn = lambda t: tf.math.exp(-r * t) lv_model = lvm.LocalVolatilityModel.from_market_data( dim=1, valuation_date=valuation_date, expiry_dates=expiry_dates, strikes=strikes, implied_volatilities=implied_volatilities, spot=initial_spot, discount_factor_fn=discount_factor_fn, dividend_yield=dividend_yield, dtype=dtype) dtype = dtype or lv_model.dtype() max_time = tf.math.reduce_max( dates.daycount_actual_365_fixed(start_date=valuation_date, end_date=expiry_dates, dtype=dtype)) if time_step is None: time_step = max_time / 100.0 rho = rho or 0.0 num_grid_points = num_grid_points or 100 leverage_fn = _leverage_function_using_pde( risk_free_rate=risk_free_rate, dividend_yield=dividend_yield, lv_model=lv_model, variance_model=variance_process, rho=[rho], initial_spot=initial_spot, initial_variance=initial_variance, time_step=time_step, max_time=max_time, num_grid_points=num_grid_points, grid_minimums=grid_minimums, grid_maximums=grid_maximums, dtype=dtype) return LocalStochasticVolatilityModel(leverage_fn, variance_process, risk_free_rate=risk_free_rate, dividend_yield=dividend_yield, rho=rho, dtype=dtype)
def _convexity_smile_replication(self, valuation_date, market, model, cms_rates, pricing_context): """Calculate CMS convexity correction by static replication.""" normal_model = ( model == rc.InterestRateModelType.NORMAL_SMILE_CONSISTENT_REPLICATION) swap_vol = tf.convert_to_tensor(pricing_context, dtype=self._dtype) expiry_time = dates.daycount_actual_365_fixed( start_date=valuation_date, end_date=self._coupon_start_dates, dtype=self._dtype) lower = tf.zeros_like(cms_rates) + 1e-6 # TODO(b/154407973): Improve the logic to compute the upper limit. rate_limit = 2000.0 upper = rate_limit * cms_rates num_points = 10001 def _call_replication(): def _intfun_call(x): d2fx = self._f_atm_second_derivative(x, cms_rates) forwards = tf.broadcast_to(tf.expand_dims(cms_rates, -1), x.shape) expiries = tf.broadcast_to(tf.expand_dims(expiry_time, -1), x.shape) option_val = _option_prices( volatilities=swap_vol, strikes=x, expiries=expiries, forwards=forwards, is_normal_model=normal_model, dtype=self._dtype) return d2fx * option_val intval_c = integration.integrate( _intfun_call, cms_rates, upper, num_points=num_points) dfk = self._f_atm_first_derivative(cms_rates, cms_rates) c_k = _option_prices(volatilities=swap_vol, strikes=cms_rates, expiries=expiry_time, forwards=cms_rates, is_normal_model=normal_model, dtype=self._dtype) return (1.0 + dfk) * c_k + intval_c def _put_replication(): def _intfun_put(x): d2fx = self._f_atm_second_derivative(x, cms_rates) forwards = tf.broadcast_to(tf.expand_dims(cms_rates, -1), x.shape) expiries = tf.broadcast_to(tf.expand_dims(expiry_time, -1), x.shape) option_val = _option_prices( volatilities=swap_vol, strikes=x, expiries=expiries, forwards=forwards, is_call_options=False, is_normal_model=normal_model, dtype=self._dtype) return d2fx * option_val intval_p = integration.integrate( _intfun_put, lower, cms_rates, num_points=num_points) dfk = self._f_atm_first_derivative(cms_rates, cms_rates) p_k = _option_prices(volatilities=swap_vol, strikes=cms_rates, expiries=expiry_time, forwards=cms_rates, is_call_options=False, is_normal_model=normal_model, dtype=self._dtype) return (1.0 + dfk) * p_k - intval_p call_rep = _call_replication() put_rep = _put_replication() return cms_rates + (call_rep - put_rep)
def _price_hull_white_1_factor(self, valuation_date, market, strike, expiry_time): """Price the swaption using Hull-White 1-factor model.""" if isinstance( self._swap.pay_leg(), cashflow_streams.FloatingCashflowStream): floating_leg = self._swap.pay_leg() fixed_leg = self._swap.receive_leg() else: fixed_leg = self._swap.pay_leg() floating_leg = self._swap.receive_leg() # Get the reference curve from the floating leg of the underlying swap reference_curve = market.yield_curve(floating_leg.reference_curve_type[0]) valuation_date_ordinal = tf.cast(valuation_date.ordinal(), dtype=self._dtype) def _refercence_rate_fn(t): # The input `t` is a real `Tensor` specifying the time from valuation. # We convert it into a `DateTensor` by first conversting it into the # corresponding ordinal (assuming ACT_365 convention). interpolation_ordinals = tf.cast( tf.round(t * 365.0 + valuation_date_ordinal), dtype=tf.int32) interpolation_dates = dateslib.convert_to_date_tensor( interpolation_ordinals) return reference_curve.discount_rate(interpolation_dates) floating_leg_start_times = dateslib.daycount_actual_365_fixed( start_date=valuation_date, end_date=floating_leg.coupon_start_dates, dtype=self._dtype) floating_leg_end_times = dateslib.daycount_actual_365_fixed( start_date=valuation_date, end_date=floating_leg.coupon_end_dates, dtype=self._dtype) fixed_leg_payment_times = dateslib.daycount_actual_365_fixed( start_date=valuation_date, end_date=fixed_leg.cashflow_dates, dtype=self._dtype) # Add the extra dimension corresponding to multiple payments in the fixed # leg. fixed_leg_coupon = tf.broadcast_to(tf.expand_dims(strike, axis=-1), fixed_leg_payment_times.shape) is_payer_swaption = tf.convert_to_tensor( isinstance(self._swap.pay_leg(), cashflow_streams.FixedCashflowStream), dtype=tf.bool) notional = self._swap.pay_leg().notional hw_price = hull_white.swaption_price( expiries=expiry_time, floating_leg_start_times=floating_leg_start_times, floating_leg_end_times=floating_leg_end_times, fixed_leg_payment_times=fixed_leg_payment_times, floating_leg_daycount_fractions=floating_leg.daycount_fractions, fixed_leg_daycount_fractions=fixed_leg.daycount_fractions, fixed_leg_coupon=fixed_leg_coupon, reference_rate_fn=_refercence_rate_fn, is_payer_swaption=is_payer_swaption, use_analytic_pricing=True, notional=notional, dim=1, mean_reversion=self._config.model_params.mean_reversion, volatility=self._config.model_params.volatility, dtype=self._dtype) return hw_price