Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH/BUG: Add spec argument for IndexFixedRateBond #93

Merged
merged 7 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/source/i_whatsnew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ email contact through **[email protected]**.
* - Bug
- Update :meth:`~rateslib.instruments.STIRFuture.analytic_delta` for
:class:`~rateslib.instruments.STIRFuture` to match *delta*.
* - Bug
- Add the ``spec`` argument functionality missing for
:class:`~rateslib.instruments.IndexFixedRateBond`.

0.6.0 (19th Oct 2023)
**********************
Expand Down
2 changes: 2 additions & 0 deletions rateslib/_spec_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def _get_kwargs(spec):
"usd_gb": _get_kwargs("usd_gb"), # FRB
"usd_gbb": _get_kwargs("usd_gbb"),
"gbp_gb": _get_kwargs("gbp_gb"),
"gbp_gbi": _get_kwargs("gbp_gbi"),
"cad_gb": _get_kwargs("cad_gb"),
"sek_gb": _get_kwargs("sek_gb"),
"sek_gbb": _get_kwargs("sek_gbb"),
Expand All @@ -136,6 +137,7 @@ def _get_kwargs(spec):
"ust": INSTRUMENT_SPECS["usd_gb"],
"ustb": INSTRUMENT_SPECS["usd_gbb"],
"ukt": INSTRUMENT_SPECS["gbp_gb"],
"ukti": INSTRUMENT_SPECS["gbp_gbi"],
"gilt": INSTRUMENT_SPECS["gbp_gb"],
"cadgb": INSTRUMENT_SPECS["cad_gb"],
"sgb": INSTRUMENT_SPECS["sek_gb"],
Expand Down
146 changes: 73 additions & 73 deletions rateslib/data/instrument_spec.csv

Large diffs are not rendered by default.

88 changes: 69 additions & 19 deletions rateslib/instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -1852,7 +1852,9 @@ class FixedRateBond(Sensitivities, BondMixin, BaseMixin):
The adjusted or unadjusted termination date. If a string, then a tenor must be
given expressed in days (`"D"`), months (`"M"`) or years (`"Y"`), e.g. `"48M"`.
frequency : str in {"M", "B", "Q", "T", "S", "A"}, optional
The frequency of the schedule. "Z" is not permitted.
The frequency of the schedule. "Z" is **not** permitted. For zero-coupon-bonds use a
``fixed_rate`` of zero and set the frequency according to the yield-to-maturity
convention required.
stub : str combining {"SHORT", "LONG"} with {"FRONT", "BACK"}, optional
The stub type to enact on the swap. Can provide two types, for
example "SHORTFRONTLONGBACK".
Expand Down Expand Up @@ -2658,6 +2660,37 @@ def gamma(self, *args, **kwargs):


class IndexFixedRateBond(FixedRateBond):
# TODO (mid) ensure calculations work for amortizing bonds.
"""
Create an indexed fixed rate bond security.

Parameters
----------
args : tuple
Required positional args for :class:`~rateslib.instruments.FixedRateBond`.
index_base : float or None, optional
The base index applied to all periods.
index_fixings : float, or Series, optional
If a float scalar, will be applied as the index fixing for the first
period.
If a list of *n* fixings will be used as the index fixings for the first *n*
periods.
If a datetime indexed ``Series`` will use the fixings that are available in
that object, and derive the rest from the ``curve``.
index_method : str
Whether the indexing uses a daily measure for settlement or the most recently
monthly data taken from the first day of month.
index_lag : int, optional
The number of months by which the index value is lagged. Used to ensure
consistency between curves and forecast values. Defined by default.
kwargs : dict
Required keyword args for :class:`~rateslib.instruments.FixedRateBond`.

Examples
--------
See :class:`~rateslib.instruments.FixedRateBond` for similar.
"""

_fixed_rate_mixin = True
_ytm_attribute = "real_cashflow" # index linked bonds use real cashflows
_index_base_mixin = True
Expand Down Expand Up @@ -2688,15 +2721,8 @@ def __init__(
settle: Union[int, NoInput] = NoInput(0),
calc_mode: Union[str, NoInput] = NoInput(0),
curves: Union[list, str, Curve, NoInput] = NoInput(0),
spec: Union[str, NoInput] = NoInput(0),
):
self.curves = curves
self.calc_mode = defaults.calc_mode if calc_mode is NoInput.blank else calc_mode.lower()
if frequency.lower() == "z":
raise ValueError("IndexFixedRateBond `frequency` must be in {M, B, Q, T, S, A}.")
if payment_lag is NoInput.blank:
payment_lag = defaults.payment_lag_specific[type(self).__name__]
self._fixed_rate = fixed_rate
self._index_base = index_base
self.kwargs = dict(
effective=effective,
termination=termination,
Expand All @@ -2714,25 +2740,49 @@ def __init__(
amortization=amortization,
convention=convention,
fixed_rate=fixed_rate,
initial_exchange=False,
final_exchange=True,
initial_exchange=NoInput(0),
final_exchange=NoInput(0),
ex_div=ex_div,
settle=settle,
calc_mode=calc_mode,
index_base=index_base,
index_method=index_method,
index_lag=index_lag,
index_fixings=index_fixings,
)
self.leg1 = IndexFixedLeg(**_get(self.kwargs, leg=1))
self.kwargs.update(
dict(
ex_div=defaults.ex_div if ex_div is NoInput.blank else ex_div,
settle=defaults.settle if settle is NoInput.blank else settle,
)
self.kwargs = _push(spec, self.kwargs)

# set defaults for missing values
default_kwargs = dict(
calc_mode=defaults.calc_mode,
initial_exchange=False,
final_exchange=True,
payment_lag=defaults.payment_lag_specific[type(self).__name__],
ex_div=defaults.ex_div,
settle=defaults.settle,
index_method=defaults.index_method,
index_lag=defaults.index_lag,
)
self.kwargs = _update_with_defaults(self.kwargs, default_kwargs)

if self.kwargs["frequency"] is NoInput.blank:
raise ValueError("`frequency` must be provided for Bond.")
# elif self.kwargs["frequency"].lower() == "z":
# raise ValueError("FixedRateBond `frequency` must be in {M, B, Q, T, S, A}.")

self.calc_mode = self.kwargs["calc_mode"].lower()
self.curves = curves
self.spec = spec

self._fixed_rate = fixed_rate
self._index_base = index_base

self.leg1 = IndexFixedLeg(**_get(self.kwargs, leg=1, filter=["ex_div", "settle", "calc_mode"]))
if self.leg1.amortization != 0:
# Note if amortization is added to FixedRateBonds must systematically
# Note if amortization is added to IndexFixedRateBonds must systematically
# go through and update all methods. Many rely on the quantity
# self.notional which is currently assumed to be a fixed quantity
raise NotImplementedError("`amortization` for FixedRateBond must be zero.")
raise NotImplementedError("`amortization` for IndexFixedRateBond must be zero.")

def index_ratio(self, settlement: datetime, curve: Union[IndexCurve, NoInput]):
if self.leg1.index_fixings is not NoInput.blank and not isinstance(
Expand Down
2 changes: 1 addition & 1 deletion rateslib/periods.py
Original file line number Diff line number Diff line change
Expand Up @@ -1947,7 +1947,7 @@ def _index_value_from_curve(
):
raise TypeError("`index_value` must be forecast from an `IndexCurve`.")
elif i_lag != i_curve.index_lag:
return None # TODO decide if RolledCurve to correct index lag be attemoted
return None # TODO decide if RolledCurve to correct index lag be attempted
else:
return i_curve.index_value(i_date, i_method)

Expand Down
15 changes: 15 additions & 0 deletions tests/test_instruments.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from rateslib.default import NoInput
from rateslib.instruments import (
FixedRateBond,
IndexFixedRateBond,
FloatRateNote,
Bill,
IRS,
Expand Down Expand Up @@ -2779,6 +2780,20 @@ def test_fixedratebond(self):
assert bond.kwargs["fixed_rate"] == 2.0
assert bond.kwargs["ex_div"] == 1

def test_indexfixedratebond(self):
bond = IndexFixedRateBond(
effective=dt(2022, 1, 1),
termination="1Y",
spec="ukti",
calc_mode="ust",
fixed_rate=2.0
)
assert bond.calc_mode == "ust"
assert bond.kwargs["convention"] == "actacticma"
assert bond.kwargs["currency"] == "gbp"
assert bond.kwargs["fixed_rate"] == 2.0
assert bond.kwargs["ex_div"] == 7

def test_bill(self):
bill = Bill(
effective=dt(2022, 1, 1),
Expand Down
5 changes: 3 additions & 2 deletions tests/test_instruments_bonds.py
Original file line number Diff line number Diff line change
Expand Up @@ -817,9 +817,10 @@ def test_fixed_rate_bond_price(self):
assert abs(bond.price(4.634, dt(1999, 5, 18), True) - 110.058738) < 1e-6
assert abs(bond.price(4.634, dt(1999, 5, 26), True) - 110.170218) < 1e-6

@pytest.mark.skip(reason="Frequency of zero calculates but is wrong. Docs do not allow.")
def test_fixed_rate_bond_zero_frequency_raises(self):
with pytest.raises(ValueError, match="FixedRateBond `frequency`"):
IndexFixedRateBond(dt(1999, 5, 7), dt(2002, 12, 7), "Z", convention="ActActICMA")
with pytest.raises(ValueError, match="`frequency` must be provided"):
IndexFixedRateBond(dt(1999, 5, 7), dt(2002, 12, 7), "Z", convention="ActActICMA", fixed_rate=1.0)

def test_fixed_rate_bond_no_amortization(self):
with pytest.raises(NotImplementedError, match="`amortization` for"):
Expand Down