Skip to content

Commit

Permalink
ENH: add averaging to FloatPeriod (#28)
Browse files Browse the repository at this point in the history
  • Loading branch information
attack68 authored Aug 20, 2023
1 parent 287a799 commit add7487
Show file tree
Hide file tree
Showing 4 changed files with 288 additions and 25 deletions.
3 changes: 3 additions & 0 deletions docs/source/i_whatsnew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ email contact through **[email protected]**.

* - Feature
- Description
* - Periods
- :class:`~rateslib.periods.FloatPeriod` now allows **averaging** methods for
determining the rate.
* - Curves
- The :meth:`shift()<rateslib.curves.Curve.shift>` operation for *Curves* now defaults to using
a *CompositeCurve* approach to preserve a constant spread to the underlying *Curve* via
Expand Down
4 changes: 4 additions & 0 deletions rateslib/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ class Defaults:
"rfr_observation_shift": 2,
"rfr_lockout": 2,
"rfr_lookback": 2,
"rfr_payment_delay_avg": 0, # no observation shift - use payment_delay param
"rfr_observation_shift_avg": 2,
"rfr_lockout_avg": 2,
"rfr_lookback_avg": 2,
"ibor": 2,
}
spread_compound_method = "none_simple"
Expand Down
68 changes: 56 additions & 12 deletions rateslib/periods.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,10 @@ def _validate_float_args(
"rfr_observation_shift",
"rfr_lockout",
"rfr_lookback",
"rfr_payment_delay_avg",
"rfr_observation_shift_avg",
"rfr_lockout_avg",
"rfr_lookback_avg",
]:
raise ValueError(
"`fixing_method` must be in {'rfr_payment_delay', "
Expand Down Expand Up @@ -611,6 +615,10 @@ class FloatPeriod(BasePeriod):
``method_param`` as integer number of business days defines the
observation offset, the DCFs remain static, measured between the start and end
dates.
- **"rfr_payment_delay_avg", "rfr_observation_shift_avg", "rfr_lockout_avg",
"rfr_lookback_avg"**: these are the same as the previous conventions except that
the period rate is defined as the arithmetic average of the individual fixings,
weighted by the relevant DCF depending upon the method.
- **"ibor"**: this the convention for determining IBOR rates from a curve. The
``method_param`` is the number of fixing lag days before the accrual start when
the fixing is published. For example, Euribor or Stibor have 2.
Expand Down Expand Up @@ -969,6 +977,7 @@ def _rfr_rate_from_df_curve(self, curve: Curve):
# TODO: (low:perf) semi-efficient method for lockout under certain conditions
else:
# return inefficient calculation
# this is also the path for all averaging methods
return self._rfr_fixings_array(curve, fixing_exposure=False)[0]

def _ibor_rate_from_df_curve(self, curve: Curve):
Expand Down Expand Up @@ -1009,6 +1018,30 @@ def _ibor_rate_from_line_curve(self, curve: LineCurve):
def _rfr_rate_from_line_curve(self, curve: LineCurve):
return self._rfr_fixings_array(curve, fixing_exposure=False)[0]

def _avg_rate_with_spread(self, rates, dcf_vals):
"""
Calculate all in rate with float spread under averaging.
Parameters
----------
rates : Series
The rates which are expected for each daily period.
dcf_vals : Series
The weightings which are used for each rate in the compounding formula.
Returns
-------
float, Dual, Dual2
"""
dcf_vals = dcf_vals.set_axis(rates.index)
if self.spread_compound_method != "none_simple":
raise ValueError(
"`spread_compound` method must be 'none_simple' in an RFR averaging "
"period."
)
else:
return (dcf_vals * rates).sum() / dcf_vals.sum() + self.float_spread / 100

def _isda_compounded_rate_with_spread(self, rates, dcf_vals):
"""
Calculate all in rates with float spread under different compounding methods.
Expand Down Expand Up @@ -1311,9 +1344,12 @@ def _rfr_fixings_array(
"rfr_payment_delay",
"rfr_observation_shift",
"rfr_lockout",
"rfr_payment_delay_avg",
"rfr_observation_shift_avg",
"rfr_lockout_avg",
]: # for all these methods there is no shift
dcf_of_r = dcf_vals.copy()
elif self.fixing_method == "rfr_lookback":
elif self.fixing_method in ["rfr_lookback", "rfr_lookback_avg"]:
dcf_of_r = Series(
[
dcf(obs_dates[i], obs_dates[i + 1], curve.convention)
Expand All @@ -1322,7 +1358,7 @@ def _rfr_fixings_array(
)
v_with_r = Series([disc_curve[obs_dates[i]] for i in range(1, len(dcf_dates.index))])

if self.fixing_method == "rfr_lockout":
if self.fixing_method in ["rfr_lockout", "rfr_lockout_avg"]:
# adjust the final rates values of the lockout arrays according to param
try:
rates.iloc[-self.method_param :] = rates.iloc[-self.method_param - 1]
Expand All @@ -1334,9 +1370,12 @@ def _rfr_fixings_array(
[Dual(float(r), f"fixing_{i}") for i, (k, r) in enumerate(rates.items())],
index=rates.index,
)
if self.fixing_method == "rfr_lockout":
if self.fixing_method in ["rfr_lockout", "rfr_lockout_avg"]:
rates_dual.iloc[-self.method_param :] = rates_dual.iloc[-self.method_param - 1]
rate = self._isda_compounded_rate_with_spread(rates_dual, dcf_vals)
if "avg" in self.fixing_method:
rate = self._avg_rate_with_spread(rates_dual, dcf_vals)
else:
rate = self._isda_compounded_rate_with_spread(rates_dual, dcf_vals)
notional_exposure = Series(
[rate.gradient(f"fixing_{i}")[0] for i in range(len(dcf_dates.index) - 1)]
)
Expand All @@ -1349,7 +1388,10 @@ def _rfr_fixings_array(
"notional": notional_exposure.apply(float, convert_dtype=float),
}
else:
rate = self._isda_compounded_rate_with_spread(rates, dcf_vals)
if "avg" in self.fixing_method:
rate = self._avg_rate_with_spread(rates, dcf_vals)
else:
rate = self._isda_compounded_rate_with_spread(rates, dcf_vals)
extra_cols = {}

if rates.isna().any():
Expand Down Expand Up @@ -1401,13 +1443,12 @@ def _fixings_table_fast(self, curve: Union[Curve, LineCurve], disc_curve: Curve)
v_vals = Series(np.exp(v_vals.to_numpy()), index=obs_vals.index)

scalar = dcf_vals.values / obs_vals.values
if self.fixing_method == "rfr_lockout":
if self.fixing_method in ["rfr_lockout", "rfr_lockout_avg"]:
scalar[-self.method_param :] = 0.0
scalar[-(self.method_param + 1)] = (
obs_vals.iloc[-(self.method_param + 1) :].sum()
/ obs_vals.iloc[-(self.method_param + 1)]
)

# perform an efficient rate approximation
rate = curve.rate(
effective=obs_dates.iloc[0],
Expand All @@ -1418,7 +1459,9 @@ def _fixings_table_fast(self, curve: Union[Curve, LineCurve], disc_curve: Curve)
)
# approximate sensitivity to each fixing
z = self.float_spread / 10000
if self.spread_compound_method == "none_simple":
if "avg" in self.fixing_method:
drdri = (1 / n)
elif self.spread_compound_method == "none_simple":
drdri = (1 / n) * (1 + (r_bar / 100) * d) ** (n - 1)
elif self.spread_compound_method == "isda_compounding":
drdri = (1 / n) * (1 + (r_bar / 100 + z) * d) ** (n - 1)
Expand Down Expand Up @@ -1495,21 +1538,22 @@ def _is_inefficient(self):

def _get_method_dcf_markers(self, curve: Union[Curve, LineCurve], endpoints=False):
# Depending upon method get the observation dates and dcf dates
if self.fixing_method in ["rfr_payment_delay", "rfr_lockout"]:
if self.fixing_method in ["rfr_payment_delay", "rfr_lockout", "rfr_payment_delay_avg", "rfr_lockout_avg"]:
start_obs, end_obs = self.start, self.end
start_dcf, end_dcf = self.start, self.end
elif self.fixing_method == "rfr_observation_shift":
elif self.fixing_method in ["rfr_observation_shift", "rfr_observation_shift_avg"]:
start_obs = add_tenor(self.start, f"-{self.method_param}b", "P", curve.calendar)
end_obs = add_tenor(self.end, f"-{self.method_param}b", "P", curve.calendar)
start_dcf, end_dcf = start_obs, end_obs
elif self.fixing_method == "rfr_lookback":
elif self.fixing_method in ["rfr_lookback", "rfr_lookback_avg"]:
start_obs = add_tenor(self.start, f"-{self.method_param}b", "P", curve.calendar)
end_obs = add_tenor(self.end, f"-{self.method_param}b", "P", curve.calendar)
start_dcf, end_dcf = self.start, self.end
else:
raise NotImplementedError(
"`fixing_method` should be in {'rfr_payment_delay', 'rfr_lockout', "
"'rfr_lookback', 'rfr_observation_shift'}"
"'rfr_lookback', 'rfr_observation_shift'} or the same with '_avg' as "
"a suffix for averaging methods."
)

if endpoints:
Expand Down
Loading

0 comments on commit add7487

Please sign in to comment.