diff --git a/python/rateslib/volatility/ir/sabr.py b/python/rateslib/volatility/ir/sabr.py index 95331d25d..6f62bf340 100644 --- a/python/rateslib/volatility/ir/sabr.py +++ b/python/rateslib/volatility/ir/sabr.py @@ -124,6 +124,9 @@ class IRSabrSmile(_BaseIRSmile, _WithMutability): shift: float, Variable, :green:`optional (set as zero)` The number of basis points to apply to the strike and forward under a 'Black Shifted Volatility' model. + time_scalar: float, Dual, Dual2, Variable, :green:`optional (set as one)` + A quantity to remap calendar day time to expiry from ``eval_date`` to another measure + of time. id: str, optional, :green:`optional (set as random)` The unique identifier to distinguish between *Smiles* in a multicurrency framework and/or *Surface*. @@ -167,6 +170,7 @@ def __init__( tenor: datetime | str, *, shift: DualTypes_ = NoInput(0), + time_scalar: DualTypes_ = NoInput(0), id: str | NoInput = NoInput(0), # noqa: A002 ad: int | None = 0, ): @@ -182,6 +186,7 @@ def __init__( _plot_y_axis="black_vol", _shift=_drb(0.0, shift), _pricing_model=OptionPricingModel.Black76, + _time_scalar=_drb(1.0, time_scalar), ) try: @@ -406,7 +411,7 @@ def _get_from_strike(self, k: DualTypes, f: DualTypes) -> _IRVolPricingParams: vol_ = _SabrModel._d_sabr_d_k_or_f( _to_number(k + self.meta.rate_shift), _to_number(f + self.meta.rate_shift), - self._meta.t_expiry, + _to_number(self._meta.t_expiry), self.nodes.alpha, self.nodes.beta, self.nodes.rho, @@ -433,7 +438,7 @@ def _d_sigma_d_f( return _SabrModel._d_sabr_d_k_or_f( # type: ignore[return-value] _to_number(k + self.meta.rate_shift), _to_number(f + self.meta.rate_shift), - self._meta.t_expiry, + _to_number(self._meta.t_expiry), self.nodes.alpha, self.nodes.beta, self.nodes.rho, diff --git a/python/rateslib/volatility/ir/spline.py b/python/rateslib/volatility/ir/spline.py index a33184ae7..6c575dd45 100644 --- a/python/rateslib/volatility/ir/spline.py +++ b/python/rateslib/volatility/ir/spline.py @@ -338,6 +338,9 @@ class IRSplineSmile(_BaseIRSmile, _WithMutability): in the appropriate form for the model. shift: float, :green:`optional (set as zero)` The shift applied to the forward and strike in pricing formula or in plot conversions. + time_scalar: float, Dual, Dual2, Variable, :green:`optional (set as one)` + A quantity to remap calendar day time to expiry from ``eval_date`` to another measure + of time. id: str, optional, :green:`optional (set as random)` The unique identifier to distinguish between *Smiles* in a multicurrency framework and/or *Surface*. @@ -382,6 +385,7 @@ def __init__( k: int_ = NoInput(0), pricing_model: OptionPricingModel | str = "normal_vol", shift: DualTypes_ = NoInput(0), + time_scalar: DualTypes_ = NoInput(0), id: str | NoInput = NoInput(0), # noqa: A002 ad: int | None = 0, ): @@ -403,6 +407,7 @@ def __init__( _plot_y_axis="normal_vol", _shift=_drb(0.0, shift), _pricing_model=_get_option_pricing_model(pricing_model), + _time_scalar=_drb(1.0, time_scalar), ) self._nodes = _IRSplineSmileNodes(nodes=nodes, k=k_) diff --git a/python/rateslib/volatility/ir/utils.py b/python/rateslib/volatility/ir/utils.py index 8beddbbe8..823c3f59f 100644 --- a/python/rateslib/volatility/ir/utils.py +++ b/python/rateslib/volatility/ir/utils.py @@ -81,6 +81,7 @@ def __init__( _plot_x_axis: str, _plot_y_axis: str, _pricing_model: OptionPricingModel, + _time_scalar: DualTypes, ): self._eval_date = _eval_date self._expiry_input = _expiry_input @@ -88,6 +89,7 @@ def __init__( self._irs_series = _irs_series self._plot_x_axis = _plot_x_axis self._plot_y_axis = _plot_y_axis + self._time_scalar = _time_scalar self._irs_fixing = IRSFixing( irs_series=self.irs_series, publication=self.expiry, @@ -98,6 +100,11 @@ def __init__( self._shift = _shift self._pricing_model = _pricing_model + @property + def time_scalar(self) -> DualTypes: + """A quantity to multiple calendar day time to expiry to remap time.""" + return self._time_scalar + @property def pricing_model(self) -> OptionPricingModel: """The option pricing model associated with this *Smile* volatility output.""" @@ -169,18 +176,18 @@ def irs_fixing(self) -> IRSFixing: return self._irs_fixing @cached_property - def t_expiry(self) -> float: - """Calendar days from eval to expiry divided by 365.""" - return (self.expiry - self.eval_date).days / 365.0 + def t_expiry(self) -> DualTypes: + """Calendar days from eval to expiry divided by 365 multiplied by remapping.""" + return (self.expiry - self.eval_date).days / 365.0 * self.time_scalar - def _t_expiry(self, expiry: datetime) -> float: - """Calendar days from eval to specified expiry divided by 365.""" - return (expiry - self.eval_date).days / 365.0 + def _t_expiry(self, expiry: datetime) -> DualTypes: + """Calendar days from eval to specified expiry divided by 365 multiplied by remapping.""" + return (expiry - self.eval_date).days / 365.0 * self.time_scalar @cached_property - def t_expiry_sqrt(self) -> float: + def t_expiry_sqrt(self) -> DualTypes: """Square root of ``t_expiry``.""" - ret: float = self.t_expiry**0.5 + ret: DualTypes = self.t_expiry**0.5 return ret diff --git a/python/tests/test_ir_volatility.py b/python/tests/test_ir_volatility.py index ae0de9c93..94007a186 100644 --- a/python/tests/test_ir_volatility.py +++ b/python/tests/test_ir_volatility.py @@ -1160,6 +1160,19 @@ def test_d_sigma_d_f(self): manual_gradient = gradient(manual.vol, ["f"])[0] / 100.0 assert abs(result - manual_gradient) < 2e-3 + def test_time_scalar(self): + irss = IRSabrSmile( + eval_date=dt(2000, 1, 1), + expiry=dt(2000, 7, 1), + tenor="1y", + irs_series="usd_irs", + beta=0.5, + nodes=dict(alpha=0.2, rho=-0.05, nu=0.65), + shift=0.0, + time_scalar=0.9, + ) + assert irss.meta.t_expiry == 0.9 * (31 + 29 + 31 + 30 + 31 + 30) / 365 + class TestIRSabrCube: def test_init(self): @@ -1629,6 +1642,19 @@ def test_d_sigma_d_f(self, model, k): manual_gradient = gradient(dual, ["f"])[0] / 100.0 assert abs(result - manual_gradient) < 1e-10 + def test_time_scalar(self): + irss = IRSplineSmile( + nodes={-200.0: 70.0, -100.0: 58, 0: 50.0, 100.0: 61, 200.0: 75.0}, + k=2, + eval_date=dt(2000, 1, 1), + expiry=dt(2000, 7, 1), + irs_series="eur_irs6", + tenor="2y", + id="vol", + time_scalar=0.9, + ) + assert irss.meta.t_expiry == 0.9 * (31 + 29 + 31 + 30 + 31 + 30) / 365 + class TestIRSplineCube: def test_init(self):