Skip to content

Commit e4fbc19

Browse files
mikelyncattack68
andauthored
ENH: add FixedRateBond components (#164) (#1087)
Co-authored-by: JHM Darbyshire <[email protected]> Co-authored-by: Mike Lync <[email protected]> Co-authored-by: JHM Darbyshire (M1) <[email protected]>
1 parent 910e729 commit e4fbc19

File tree

27 files changed

+6847
-80
lines changed

27 files changed

+6847
-80
lines changed

python/rateslib/instruments/components/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from rateslib.instruments.components.bonds import FixedRateBond
12
from rateslib.instruments.components.cds import CDS
23
from rateslib.instruments.components.fly import Fly
34
from rateslib.instruments.components.fra import FRA
@@ -34,4 +35,5 @@
3435
"FXForward",
3536
"FXVolValue",
3637
"FXSwap",
38+
"FixedRateBond",
3739
]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from rateslib.instruments.components.bonds.fixed_rate_bond import FixedRateBond
2+
3+
__all__ = ["FixedRateBond"]

python/rateslib/instruments/components/bonds/conventions/__init__.py

Lines changed: 732 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime
4+
from typing import TYPE_CHECKING, Protocol
5+
6+
from rateslib.scheduling import dcf
7+
8+
if TYPE_CHECKING:
9+
from rateslib.typing import ( # pragma: no cover
10+
Any,
11+
BondMixin,
12+
Security,
13+
_SupportsFixedFloatLeg1,
14+
)
15+
16+
"""
17+
All functions in this module are designed to take a Bond object and return the **fraction**
18+
of the current coupon period associated with the given settlement.
19+
20+
This fraction is used to assess the total accrued calculation at a subsequent stage.
21+
"""
22+
23+
24+
class AccrualFunction(Protocol):
25+
# Callable type for Accrual Functions
26+
def __call__(
27+
self, obj: Security | BondMixin, settlement: datetime, acc_idx: int, *args: Any
28+
) -> float: ...
29+
30+
31+
def _acc_linear_proportion_by_days(
32+
obj: _SupportsFixedFloatLeg1, settlement: datetime, acc_idx: int, *args: Any
33+
) -> float:
34+
"""
35+
Return the fraction of an accrual period between start and settlement.
36+
37+
Method: a linear proportion of actual days between start, settlement and end.
38+
Measures between unadjusted coupon dates.
39+
40+
This is a general method, used by many types of bonds, for example by UK Gilts,
41+
German Bunds.
42+
"""
43+
r = (settlement - obj.leg1.schedule.aschedule[acc_idx]).days
44+
s = (obj.leg1.schedule.aschedule[acc_idx + 1] - obj.leg1.schedule.aschedule[acc_idx]).days
45+
return float(r / s)
46+
47+
48+
def _acc_linear_proportion_by_days_long_stub_split(
49+
obj: _SupportsFixedFloatLeg1,
50+
settlement: datetime,
51+
acc_idx: int,
52+
*args: Any,
53+
) -> float:
54+
"""
55+
For long stub periods this splits the accrued interest into two components.
56+
Otherwise, returns the regular linear proportion.
57+
[Designed primarily for US Treasuries]
58+
"""
59+
# TODO: handle this union attribute by segregating Securities periods into different
60+
# categories, perhaps when also integrating deterministic amortised bonds.
61+
if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr]
62+
f = obj.leg1.schedule.periods_per_annum
63+
freq = obj.leg1.schedule.frequency_obj
64+
adjuster = obj.leg1.schedule.accrual_adjuster
65+
calendar = obj.leg1.schedule.calendar
66+
67+
if obj.leg1.periods[acc_idx].period_params.dcf * f > 1: # type: ignore[union-attr]
68+
# long stub
69+
70+
if acc_idx > 0:
71+
# then stub is implied to be at the back, must roll forwards
72+
ustart = obj.leg1.schedule.uschedule[acc_idx]
73+
astart = obj.leg1.schedule.aschedule[acc_idx]
74+
quasi_ucoupon = freq.unext(ustart)
75+
quasi_acoupon = adjuster.adjust(quasi_ucoupon, calendar)
76+
quasi_uend = freq.unext(quasi_ucoupon)
77+
quasi_aend = adjuster.adjust(quasi_uend, calendar)
78+
s_bar_u = (quasi_acoupon - astart).days
79+
80+
if settlement <= quasi_acoupon:
81+
#
82+
# |--------------------------|-----------------|---------|
83+
# s * qc e qe
84+
# <-----------s_bar_u-------->
85+
# <---r_bar_u-----------> ==> (r_bar_u / s_bar_u) / (df)
86+
r_bar_u = (settlement - astart).days
87+
r_u = 0.0
88+
s_u = 1.0
89+
else:
90+
#
91+
# |--------------------------|-----------------|---------|
92+
# s qc * e qe
93+
# <-----------s_bar_u--------><------s_u----------------->
94+
# <--------r_bar_u-----------><----r_u------>
95+
# ==> (r_bar_u / s_bar_u + r_u / s_u) / (df)
96+
r_u = (settlement - quasi_acoupon).days
97+
s_u = (quasi_aend - quasi_acoupon).days
98+
r_bar_u = (quasi_acoupon - astart).days
99+
else:
100+
# then stub is implied to be at the front, must roll backwards
101+
uend = obj.leg1.schedule.uschedule[acc_idx + 1]
102+
aend = obj.leg1.schedule.aschedule[acc_idx + 1]
103+
quasi_ucoupon = freq.uprevious(uend)
104+
quasi_acoupon = adjuster.adjust(quasi_ucoupon, calendar)
105+
quasi_ustart = freq.uprevious(quasi_ucoupon)
106+
quasi_astart = adjuster.adjust(quasi_ustart, calendar)
107+
s_bar_u = (quasi_acoupon - quasi_astart).days
108+
109+
if settlement <= quasi_acoupon:
110+
#
111+
# |--------|-------------------|--------------------------|
112+
# qs s * qc e
113+
# <-----------s_bar_u--------->
114+
# <---r_bar_u---> ==> (r_bar_u / s_bar_u) / (df)
115+
r_bar_u = (settlement - obj.leg1.schedule.aschedule[acc_idx]).days
116+
r_u = 0.0
117+
s_u = 1.0
118+
else:
119+
#
120+
# |--------|-------------------|--------------------------|
121+
# qs s qc * e
122+
# <-----------s_bar_u---------><------------s_u----------->
123+
# <-------r_bar_u----><------r_u----->
124+
#
125+
# ==> (r_bar_u / s_bar_u + r_u / s_u) / (df)
126+
r_u = (settlement - quasi_acoupon).days
127+
s_u = (aend - quasi_acoupon).days
128+
r_bar_u = (quasi_acoupon - obj.leg1.schedule.aschedule[acc_idx]).days
129+
130+
return (r_bar_u / s_bar_u + r_u / s_u) / (
131+
obj.leg1.periods[acc_idx].period_params.dcf * f
132+
) # type: ignore[union-attr]
133+
134+
return _acc_linear_proportion_by_days(obj, settlement, acc_idx, *args)
135+
136+
137+
def _acc_30e360_backward(
138+
obj: _SupportsFixedFloatLeg1, settlement: datetime, acc_idx: int, *args: Any
139+
) -> float:
140+
"""
141+
Ignoring the convention on the leg uses "30E360" to determine the accrual fraction.
142+
Measures between unadjusted date and settlement.
143+
[Designed primarily for Swedish Government Bonds]
144+
145+
If stub revert to linear proportioning.
146+
"""
147+
if obj.leg1.periods[acc_idx].period_params.stub: # type: ignore[union-attr]
148+
return _acc_linear_proportion_by_days(obj, settlement, acc_idx)
149+
f = obj.leg1.schedule.periods_per_annum
150+
_: float = (
151+
dcf(
152+
start=settlement,
153+
end=obj.leg1.schedule.aschedule[acc_idx + 1],
154+
convention="30e360",
155+
frequency=obj.leg1.schedule.frequency_obj,
156+
)
157+
* f
158+
)
159+
_ = 1 - _
160+
return _
161+
162+
163+
def _acc_30u360_forward(
164+
obj: _SupportsFixedFloatLeg1, settlement: datetime, acc_idx: int, *args: Any
165+
) -> float:
166+
"""
167+
Ignoring the convention on the leg uses "30U360" to determine the accrual fraction.
168+
Measures between unadjusted dates and settlement.
169+
[Designed primarily for US Corporate/Muni Bonds]
170+
"""
171+
sch = obj.leg1.schedule
172+
accrued = dcf(
173+
start=sch.aschedule[acc_idx],
174+
end=settlement,
175+
convention="30u360",
176+
frequency=sch.frequency_obj,
177+
)
178+
period = dcf(
179+
start=sch.aschedule[acc_idx],
180+
end=sch.aschedule[acc_idx + 1],
181+
convention="30u360",
182+
frequency=sch.frequency_obj,
183+
)
184+
return accrued / period
185+
186+
187+
def _acc_act365_with_1y_and_stub_adjustment(
188+
obj: _SupportsFixedFloatLeg1, settlement: datetime, acc_idx: int, *args: Any
189+
) -> float:
190+
"""
191+
Ignoring the convention on the leg uses "Act365f" to determine the accrual fraction.
192+
Measures between unadjusted date and settlement.
193+
Special adjustment if number of days is greater than 365.
194+
If the period is a stub reverts to a straight line interpolation
195+
[this is primarily designed for Canadian Government Bonds]
196+
"""
197+
if obj.leg1._regular_periods[acc_idx].period_params.stub: # type: ignore[union-attr]
198+
return _acc_linear_proportion_by_days(obj, settlement, acc_idx)
199+
f = obj.leg1.schedule.periods_per_annum
200+
r = (settlement - obj.leg1.schedule.aschedule[acc_idx]).days
201+
s = (obj.leg1.schedule.aschedule[acc_idx + 1] - obj.leg1.schedule.aschedule[acc_idx]).days
202+
if r == s:
203+
_: float = 1.0 # then settlement falls on the coupon date
204+
elif r >= 365.0 / f:
205+
_ = 1.0 - ((s - r) * f) / 365.0 # counts remaining days
206+
else:
207+
_ = f * r / 365.0
208+
return _
209+
210+
211+
ACC_FRAC_FUNCS: dict[str, AccrualFunction] = {
212+
"linear_days": _acc_linear_proportion_by_days,
213+
"linear_days_long_front_split": _acc_linear_proportion_by_days_long_stub_split,
214+
"30e360_backward": _acc_30e360_backward,
215+
"30u360_forward": _acc_30u360_forward,
216+
"act365f_1y": _acc_act365_with_1y_and_stub_adjustment,
217+
}

0 commit comments

Comments
 (0)