Coverage for tests / test_bonds.py: 99%
206 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-27 14:36 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-27 14:36 +0000
1from unittest import main, TestCase
2from rivapy.tools.datetools import DayCounter, Schedule, _term_to_period
3from rivapy.marketdata.fixing_table import FixingTable
4from rivapy.instruments.bond_specifications import (
5 ZeroBondSpecification,
6 FixedRateBondSpecification,
7 FloatingRateBondSpecification,
8)
9from rivapy.tools.enums import RollConvention, SecuritizationLevel, DayCounterType, Currency
10from rivapy.pricing import DeterministicCashflowPricer, price
11from rivapy.marketdata import DiscountCurveParametrized, ConstantRate
12from datetime import date, datetime
15class BondSpecificationTests(TestCase):
17 def test_bond_specification(self):
18 # zero coupon bond
19 zero_coupon_bond = ZeroBondSpecification(
20 obj_id="US500769CH58",
21 issue_price=85.0,
22 issue_date=datetime(2007, 6, 29),
23 maturity_date=datetime(2037, 6, 29),
24 currency="USD",
25 notional=1000,
26 issuer="KfW",
27 securitization_level=SecuritizationLevel.SENIOR_UNSECURED,
28 )
30 self.assertEqual(zero_coupon_bond.obj_id, "US500769CH58")
31 self.assertEqual(zero_coupon_bond.issue_date, datetime(2007, 6, 29))
32 self.assertEqual(zero_coupon_bond.maturity_date, datetime(2037, 6, 29))
33 self.assertEqual(zero_coupon_bond.currency, "USD")
34 self.assertEqual(zero_coupon_bond.notional_amount(), 1000)
35 self.assertEqual(zero_coupon_bond.issuer, "KfW")
36 self.assertEqual(zero_coupon_bond.securitization_level, "SENIOR_UNSECURED")
37 self.assertEqual(zero_coupon_bond.issue_price, 85.0)
39 # fixed rate bond
40 fixed_rate_bond = FixedRateBondSpecification(
41 obj_id="DE000CZ40NT7",
42 issue_date=datetime(2019, 3, 11),
43 maturity_date=datetime(2024, 9, 11),
44 coupon=0.0125,
45 frequency="1Y",
46 business_day_convention=RollConvention.FOLLOWING,
47 currency="EUR",
48 notional=100000,
49 issuer="Commerzbank",
50 securitization_level=SecuritizationLevel.NON_PREFERRED_SENIOR,
51 stub_type_is_Long=False,
52 )
53 self.assertEqual(fixed_rate_bond.obj_id, "DE000CZ40NT7")
54 self.assertEqual(fixed_rate_bond.issue_date, datetime(2019, 3, 11))
55 self.assertEqual(fixed_rate_bond.maturity_date, datetime(2024, 9, 11))
56 self.assertEqual(
57 fixed_rate_bond.get_schedule().generate_dates(True),
58 [
59 datetime(2019, 9, 11),
60 datetime(2020, 9, 11),
61 datetime(2021, 9, 13),
62 datetime(2022, 9, 12),
63 datetime(2023, 9, 11),
64 datetime(2024, 9, 11),
65 ],
66 )
67 # self.assertEqual(fixed_rate_bond.coupons, [0.0125, 0.0125, 0.0125, 0.0125, 0.0125, 0.0125])
68 self.assertEqual(fixed_rate_bond.currency, "EUR")
69 self.assertEqual(fixed_rate_bond.notional_amount(), 100000)
70 self.assertEqual(fixed_rate_bond.issuer, "Commerzbank")
71 self.assertEqual(fixed_rate_bond.securitization_level, "NON_PREFERRED_SENIOR")
72 fixed_rate_bond = FixedRateBondSpecification(
73 obj_id="DE000CZ40NT7",
74 issue_date=datetime(2019, 3, 11),
75 maturity_date=datetime(2024, 9, 11),
76 frequency="1Y",
77 coupon=0.0125,
78 currency="EUR",
79 notional=100000,
80 issuer="Commerzbank",
81 securitization_level=SecuritizationLevel.NON_PREFERRED_SENIOR,
82 stub_type_is_Long=False,
83 )
84 self.assertEqual(fixed_rate_bond.obj_id, "DE000CZ40NT7")
85 self.assertEqual(fixed_rate_bond.issue_date, datetime(2019, 3, 11))
86 self.assertEqual(fixed_rate_bond.maturity_date, datetime(2024, 9, 11))
87 self.assertEqual(
88 fixed_rate_bond.get_schedule().generate_dates(True),
89 [
90 datetime(2019, 9, 11),
91 datetime(2020, 9, 11),
92 datetime(2021, 9, 13),
93 datetime(2022, 9, 12),
94 datetime(2023, 9, 11),
95 datetime(2024, 9, 11),
96 ],
97 )
98 self.assertEqual(fixed_rate_bond.coupon, 0.0125)
99 self.assertEqual(fixed_rate_bond.currency, "EUR")
100 self.assertEqual(fixed_rate_bond.notional_amount(), 100000)
101 self.assertEqual(fixed_rate_bond.issuer, "Commerzbank")
102 self.assertEqual(fixed_rate_bond.securitization_level, "NON_PREFERRED_SENIOR")
104 fixed_rate_bond_amort = FixedRateBondSpecification(
105 obj_id="DE000CZ40NT7",
106 issue_date=datetime(2019, 3, 11),
107 maturity_date=datetime(2024, 9, 11),
108 frequency="1Y",
109 coupon=0.0125,
110 currency="EUR",
111 notional=100000,
112 amortization_scheme="linear",
113 issuer="Commerzbank",
114 securitization_level=SecuritizationLevel.NON_PREFERRED_SENIOR,
115 stub_type_is_Long=False,
116 )
117 self.assertEqual(fixed_rate_bond_amort.notional_amount(), 100000)
118 self.assertEqual(fixed_rate_bond_amort._notional.get_amount(), 100000)
119 self.assertEqual(fixed_rate_bond_amort._amortization_scheme.get_total_amortization(), 100.0)
120 self.assertEqual(fixed_rate_bond_amort._amortization_scheme.n_steps, 1)
121 self.assertEqual(fixed_rate_bond_amort._notional.get_size(), 1)
122 self.assertEqual(fixed_rate_bond_amort._notional.get_amortizations_by_index(), [(1, 100000.0)])
124 fixed_rate_bond_amort2 = FixedRateBondSpecification(
125 obj_id="DE000CZ40NT7",
126 issue_date=datetime(2019, 3, 11),
127 maturity_date=datetime(2024, 9, 11),
128 frequency="1Y",
129 coupon=0.0125,
130 currency="EUR",
131 notional=100000,
132 amortization_scheme="linear",
133 issuer="Commerzbank",
134 securitization_level=SecuritizationLevel.NON_PREFERRED_SENIOR,
135 stub_type_is_Long=False,
136 )
137 fixed_rate_bond_amort2._notional.n_steps = 5
138 self.assertEqual(fixed_rate_bond_amort2.notional_amount(), 100000)
139 self.assertEqual(fixed_rate_bond_amort2._notional.get_amount(), 100000)
140 self.assertEqual(fixed_rate_bond_amort2._amortization_scheme.get_total_amortization(), 100.0)
141 self.assertEqual(fixed_rate_bond_amort2._amortization_scheme.n_steps, 1)
142 self.assertEqual(fixed_rate_bond_amort2._notional.get_size(), 5)
143 self.assertEqual(fixed_rate_bond_amort2._notional.get_amortizations_by_index(), [(1, 25000.0), (2, 25000.0), (3, 25000.0), (4, 25000.0)])
145 # floating rate bond
146 floating_rate_note = FloatingRateBondSpecification(
147 obj_id="DE000HLB3DU1",
148 issue_date=datetime(2016, 6, 23),
149 maturity_date=datetime(2024, 6, 27),
150 frequency="3M",
151 adjust_start_date=False,
152 adjust_end_date=False,
153 business_day_convention=RollConvention.FOLLOWING,
154 day_count_convention=DayCounterType.ThirtyU360,
155 margin=0.0,
156 index="EURIBOR_3M",
157 currency="EUR",
158 notional=1000,
159 issuer="Helaba",
160 securitization_level=SecuritizationLevel.NON_PREFERRED_SENIOR,
161 )
162 self.assertEqual(floating_rate_note.obj_id, "DE000HLB3DU1")
163 self.assertEqual(floating_rate_note.issue_date, datetime(2016, 6, 23))
164 self.assertEqual(floating_rate_note.maturity_date, datetime(2024, 6, 27))
165 # Coupon period dates including start of first accrual period
166 self.assertEqual(
167 Schedule._roll_out(
168 floating_rate_note.maturity_date,
169 floating_rate_note.start_date,
170 floating_rate_note._frequency,
171 backwards=True,
172 ), # reverse to get in correct order
173 [
174 datetime(2016, 6, 23),
175 datetime(2016, 9, 27),
176 datetime(2016, 12, 27),
177 datetime(2017, 3, 27),
178 datetime(2017, 6, 27),
179 datetime(2017, 9, 27),
180 datetime(2017, 12, 27),
181 datetime(2018, 3, 27),
182 datetime(2018, 6, 27),
183 datetime(2018, 9, 27),
184 datetime(2018, 12, 27),
185 datetime(2019, 3, 27),
186 datetime(2019, 6, 27),
187 datetime(2019, 9, 27),
188 datetime(2019, 12, 27),
189 datetime(2020, 3, 27),
190 datetime(2020, 6, 27),
191 datetime(2020, 9, 27),
192 datetime(2020, 12, 27),
193 datetime(2021, 3, 27),
194 datetime(2021, 6, 27),
195 datetime(2021, 9, 27),
196 datetime(2021, 12, 27),
197 datetime(2022, 3, 27),
198 datetime(2022, 6, 27),
199 datetime(2022, 9, 27),
200 datetime(2022, 12, 27),
201 datetime(2023, 3, 27),
202 datetime(2023, 6, 27),
203 datetime(2023, 9, 27),
204 datetime(2023, 12, 27),
205 datetime(2024, 3, 27),
206 datetime(2024, 6, 27),
207 ],
208 )
209 # Coupon payment dates
210 self.assertEqual(
211 floating_rate_note.get_schedule().generate_dates(True),
212 [
213 datetime(2016, 9, 27),
214 datetime(2016, 12, 27),
215 datetime(2017, 3, 27),
216 datetime(2017, 6, 27),
217 datetime(2017, 9, 27),
218 datetime(2017, 12, 27),
219 datetime(2018, 3, 27),
220 datetime(2018, 6, 27),
221 datetime(2018, 9, 27),
222 datetime(2018, 12, 27),
223 datetime(2019, 3, 27),
224 datetime(2019, 6, 27),
225 datetime(2019, 9, 27),
226 datetime(2019, 12, 27),
227 datetime(2020, 3, 27),
228 datetime(2020, 6, 29),
229 datetime(2020, 9, 28),
230 datetime(2020, 12, 28),
231 datetime(2021, 3, 29),
232 datetime(2021, 6, 28),
233 datetime(2021, 9, 27),
234 datetime(2021, 12, 27),
235 datetime(2022, 3, 28),
236 datetime(2022, 6, 27),
237 datetime(2022, 9, 27),
238 datetime(2022, 12, 27),
239 datetime(2023, 3, 27),
240 datetime(2023, 6, 27),
241 datetime(2023, 9, 27),
242 datetime(2023, 12, 27),
243 datetime(2024, 3, 27),
244 datetime(2024, 6, 27),
245 ],
246 )
248 self.assertEqual(floating_rate_note._day_count_convention.value, "30U360")
249 self.assertEqual(floating_rate_note._margin, 0.0)
250 self.assertEqual(floating_rate_note._index, "EURIBOR_3M")
251 if floating_rate_note._ir_index is not None:
252 self.assertEqual(floating_rate_note._ir_index.value.name, "EURIBOR 3M")
253 else:
254 print("IR index is None.")
255 self.assertEqual(floating_rate_note.currency, "EUR")
256 self.assertEqual(floating_rate_note.notional_amount(), 1000)
257 self.assertEqual(floating_rate_note.issuer, "Helaba")
258 self.assertEqual(floating_rate_note.securitization_level, "NON_PREFERRED_SENIOR")
259 floating_rate_note = FloatingRateBondSpecification(
260 obj_id="DE000HLB3DU1",
261 issue_date=datetime(2016, 6, 23),
262 maturity_date=datetime(2024, 6, 27),
263 # coupon_period_dates=floating_rate_note.coupon_period_dates,
264 day_count_convention=DayCounterType.ThirtyU360,
265 margin=floating_rate_note._margin,
266 index="EURIBOR_3M",
267 currency="EUR",
268 notional=1000,
269 issuer="Helaba",
270 securitization_level=SecuritizationLevel.NON_PREFERRED_SENIOR,
271 )
272 self.assertEqual(floating_rate_note.obj_id, "DE000HLB3DU1")
273 self.assertEqual(floating_rate_note.issue_date, datetime(2016, 6, 23))
274 self.assertEqual(floating_rate_note.maturity_date, datetime(2024, 6, 27))
275 self.assertEqual(
276 floating_rate_note.get_schedule().generate_dates(False),
277 [
278 datetime(2016, 6, 23),
279 datetime(2016, 9, 27),
280 datetime(2016, 12, 27),
281 datetime(2017, 3, 27),
282 datetime(2017, 6, 27),
283 datetime(2017, 9, 27),
284 datetime(2017, 12, 27),
285 datetime(2018, 3, 27),
286 datetime(2018, 6, 27),
287 datetime(2018, 9, 27),
288 datetime(2018, 12, 27),
289 datetime(2019, 3, 27),
290 datetime(2019, 6, 27),
291 datetime(2019, 9, 27),
292 datetime(2019, 12, 27),
293 datetime(2020, 3, 27),
294 datetime(2020, 6, 29),
295 datetime(2020, 9, 28),
296 datetime(2020, 12, 28),
297 datetime(2021, 3, 29),
298 datetime(2021, 6, 28),
299 datetime(2021, 9, 27),
300 datetime(2021, 12, 27),
301 datetime(2022, 3, 28),
302 datetime(2022, 6, 27),
303 datetime(2022, 9, 27),
304 datetime(2022, 12, 27),
305 datetime(2023, 3, 27),
306 datetime(2023, 6, 27),
307 datetime(2023, 9, 27),
308 datetime(2023, 12, 27),
309 datetime(2024, 3, 27),
310 datetime(2024, 6, 27),
311 ],
312 )
313 self.assertEqual(floating_rate_note._day_count_convention.value, "30U360")
314 self.assertEqual(floating_rate_note._ir_index.value.name, "EURIBOR 3M")
315 self.assertEqual(floating_rate_note.currency, "EUR")
316 self.assertEqual(floating_rate_note.notional_amount(), 1000)
317 self.assertEqual(floating_rate_note.issuer, "Helaba")
318 self.assertEqual(floating_rate_note.securitization_level, "NON_PREFERRED_SENIOR")
320 # fixed-to-floating rate note
321 # if False:
322 # # not correctly working
323 # fixed_to_floating_rate_note = FixedToFloatingRateNote.from_master_data('XS1887493309', datetime(2018, 10, 4),
324 # datetime(2022, 1, 20), datetime(2023, 1, 20),
325 # 0.04247, '6M', '3M', True, True, True,
326 # False, RollConvention.MODIFIED_FOLLOWING,
327 # RollConvention.MODIFIED_FOLLOWING, 'DE',
328 # 'DE', DayCounterType.ThirtyU360, 0.0115,
329 # 'US_LIBOR_3M', 'USD', 1000000,
330 # 'Standard Chartered PLC',
331 # SecuritizationLevel.SENIOR_SECURED)
332 # self.assertEqual(fixed_to_floating_rate_note.obj_id, 'XS1887493309')
333 # # self.assertEqual(fixed_to_floating_rate_note.issue_date, datetime(2018, 10, 4))
334 # self.assertEqual(fixed_to_floating_rate_note.maturity_date, datetime(2023, 1, 20))
335 # self.assertEqual(fixed_to_floating_rate_note.coupon_payment_dates, [datetime(2019, 1, 21), datetime(2019, 7, 22),
336 # datetime(2020, 1, 20), datetime(2020, 7, 20),
337 # datetime(2021, 1, 20), datetime(2021, 7, 20),
338 # datetime(2022, 1, 20)])
339 # self.assertEqual(fixed_to_floating_rate_note.coupons, [0.04247, 0.04247, 0.04247, 0.04247, 0.04247, 0.04247,
340 # 0.04247])
341 # self.assertEqual(fixed_to_floating_rate_note.coupon_period_dates, [datetime(2022, 1, 20), datetime(2022, 4, 20),
342 # datetime(2022, 7, 20), datetime(2022, 10, 20),
343 # datetime(2023, 1, 20)])
344 # self.assertEqual(fixed_to_floating_rate_note.day_count_convention, '30U360')
345 # self.assertEqual(fixed_to_floating_rate_note.spreads, [0.0115, 0.0115, 0.0115, 0.0115])
346 # self.assertEqual(fixed_to_floating_rate_note.currency, 'USD')
347 # self.assertEqual(fixed_to_floating_rate_note.notional, 1000000)
348 # self.assertEqual(fixed_to_floating_rate_note.issuer, 'Standard Chartered PLC')
349 # self.assertEqual(fixed_to_floating_rate_note.securitization_level, 'SENIOR_SECURED')
352class BondPricingTests(TestCase):
354 def test_yield_bond(self):
355 """Simple test for yield computation: Fixed rate used to compute pv which is then used to compute yield."""
356 ref_date = datetime(2023, 5, 1)
357 bond_spec = FixedRateBondSpecification(
358 "PV_BOND",
359 issue_date=datetime(2023, 1, 1),
360 maturity_date=datetime(2025, 1, 2),
361 currency=Currency.EUR,
362 notional=100.0,
363 issuer="None",
364 securitization_level=SecuritizationLevel.SUBORDINATED,
365 coupon=0.05,
366 frequency="1Y",
367 )
368 dc = DiscountCurveParametrized("", ref_date, ConstantRate(0.05))
369 nr_annual_payments = bond_spec.get_nr_annual_payments()
370 self.assertEqual(nr_annual_payments, 1)
371 # schedule for accrual periods rolled out
372 dates = bond_spec.get_schedule()._roll_out(
373 from_=bond_spec._start_date if not bond_spec._backwards else bond_spec._end_date,
374 to_=bond_spec._end_date if not bond_spec._backwards else bond_spec._start_date,
375 term=_term_to_period(bond_spec._frequency),
376 long_stub=bond_spec._stub_type_is_Long,
377 backwards=bond_spec._backwards,
378 )
379 ## discount factors
381 df1 = dc.value(ref_date, datetime(2024, 1, 2))
382 df2 = dc.value(ref_date, datetime(2025, 1, 2))
383 dcc = DayCounter(bond_spec.day_count_convention)
384 self.assertAlmostEqual(df1, 0.9668628440577077, delta=1e-6)
385 self.assertAlmostEqual(df2, 0.9195824079027841, delta=1e-6)
386 price = df1 * 5.0 + df2 * 105.0
387 self.assertAlmostEqual(DeterministicCashflowPricer.get_pv_cashflows(ref_date, bond_spec, dc), price, delta=1e-6)
389 self.assertEqual(dates, [datetime(2023, 1, 2), datetime(2024, 1, 2), datetime(2025, 1, 2)])
390 self.assertEqual(
391 DeterministicCashflowPricer.get_expected_cashflows(bond_spec),
392 [(datetime(2023, 1, 2), -100.0), (datetime(2024, 1, 2), 5.0), (datetime(2025, 1, 2), 5.0), (datetime(2025, 1, 2), 100.0)],
393 )
394 bond_price = DeterministicCashflowPricer.get_pv_cashflows(ref_date, bond_spec, dc)
395 bond_yield = DeterministicCashflowPricer.get_compute_yield(target_dirty_price=bond_price, val_date=ref_date, specification=bond_spec)
396 self.assertAlmostEqual(bond_yield, 0.05, delta=1e-3)
397 bpricer = DeterministicCashflowPricer(ref_date, bond_spec, dc)
398 self.assertAlmostEqual(bpricer.compute_yield(bond_price), 0.05, delta=1e-3)
399 mc_duration = (
400 dcc.yf(ref_date, datetime(2024, 1, 2), bond_spec.dates, bond_spec.nr_annual_payments) * df1 * 5.0 / bond_price
401 + dcc.yf(ref_date, datetime(2025, 1, 2), bond_spec.dates, bond_spec.nr_annual_payments) * df2 * 105.0 / bond_price
402 )
403 self.assertAlmostEqual(bpricer.macaulay_duration(), mc_duration, delta=1e-3)
404 self.assertAlmostEqual(bpricer.z_spread(bond_price), 0.0, delta=1e-6)
405 self.assertAlmostEqual(bpricer.macaulay_duration(), mc_duration, delta=1e-3)
406 self.assertAlmostEqual(bpricer.modified_duration(bond_price), mc_duration / 1.05, delta=1e-3)
408 def test_float_bond(self):
409 ref_date = datetime(2023, 5, 1)
410 fix_table = FixingTable("PV_BOND")
411 fix_table.add("1Y", datetime(2022, 12, 30), 0.05)
412 float_bond_spec = FloatingRateBondSpecification(
413 "PV_BOND",
414 issue_date=datetime(2023, 1, 1),
415 maturity_date=datetime(2025, 1, 2),
416 currency=Currency.EUR,
417 notional=100.0,
418 issuer="None",
419 securitization_level=SecuritizationLevel.SUBORDINATED,
420 day_count_convention="ACTACTICMA",
421 business_day_convention="ModifiedFollowing",
422 margin=0.00,
423 frequency="1Y",
424 fixings=fix_table,
425 )
426 dc = DiscountCurveParametrized("", ref_date, ConstantRate(0.05))
427 nr_annual_payments = float_bond_spec.get_nr_annual_payments()
428 self.assertEqual(nr_annual_payments, 1)
429 dates = float_bond_spec.get_schedule()._roll_out(
430 from_=float_bond_spec._start_date if not float_bond_spec._backwards else float_bond_spec._end_date,
431 to_=float_bond_spec._end_date if not float_bond_spec._backwards else float_bond_spec._start_date,
432 term=_term_to_period(float_bond_spec._frequency),
433 long_stub=float_bond_spec._stub_type_is_Long,
434 backwards=float_bond_spec._backwards,
435 )
436 ## discount factors
438 df1 = dc.value(ref_date, datetime(2024, 1, 2))
439 df2 = dc.value(ref_date, datetime(2025, 1, 2))
440 dcc = DayCounter(float_bond_spec.day_count_convention)
441 self.assertAlmostEqual(df1, 0.9668628440577077, delta=1e-6)
442 self.assertAlmostEqual(df2, 0.9195824079027841, delta=1e-6)
443 price = df1 * 5.0 + df2 * 105.0
444 print(DeterministicCashflowPricer.get_expected_cashflows(float_bond_spec, ref_date, dc))
445 self.assertEqual(dates, [datetime(2023, 1, 2), datetime(2024, 1, 2), datetime(2025, 1, 2)])
446 expected = [
447 (datetime(2023, 1, 2), -100.0),
448 (datetime(2024, 1, 2), 5.0),
449 (datetime(2025, 1, 2), 5.0),
450 (datetime(2025, 1, 2), 100.0),
451 ]
452 actual = DeterministicCashflowPricer.get_expected_cashflows(float_bond_spec, ref_date, dc)
454 self.assertEqual(len(actual), len(expected))
455 for i, (exp, act) in enumerate(zip(expected, actual)):
456 with self.subTest(i=i):
457 # exact equality for the date
458 self.assertEqual(exp[0], act[0])
459 # almost-equal for the amount with a tolerance (choose places or delta)
460 self.assertAlmostEqual(exp[1], act[1], places=6)
461 self.assertAlmostEqual(DeterministicCashflowPricer.get_pv_cashflows(ref_date, float_bond_spec, dc, dc), price, delta=1e-6)
462 bond_yield = DeterministicCashflowPricer.get_compute_yield(
463 target_dirty_price=price, val_date=ref_date, specification=float_bond_spec, fwd_curve=dc
464 )
465 self.assertAlmostEqual(bond_yield, 0.05, delta=1e-3)
466 bpricer = DeterministicCashflowPricer(ref_date, float_bond_spec, dc, dc)
467 self.assertAlmostEqual(bpricer.compute_yield(price), 0.05, delta=1e-3)
468 mc_duration = (
469 dcc.yf(ref_date, datetime(2024, 1, 2), float_bond_spec.dates, float_bond_spec.nr_annual_payments) * df1 * 5.0 / price
470 + dcc.yf(ref_date, datetime(2025, 1, 2), float_bond_spec.dates, float_bond_spec.nr_annual_payments) * df2 * 105.0 / price
471 )
472 self.assertAlmostEqual(bpricer.macaulay_duration(), mc_duration, delta=1e-3)
473 self.assertAlmostEqual(bpricer.z_spread(price), 0.0, delta=1e-6)
474 self.assertAlmostEqual(bpricer.macaulay_duration(), mc_duration, delta=1e-3)
475 self.assertAlmostEqual(bpricer.modified_duration(price), mc_duration / 1.05, delta=1e-3)
477 float_bond_spec_2 = FloatingRateBondSpecification(
478 "PV_BOND",
479 issue_date=datetime(2023, 1, 1),
480 maturity_date=datetime(2025, 1, 1),
481 currency=Currency.EUR,
482 notional=100.0,
483 issuer="None",
484 securitization_level=SecuritizationLevel.SUBORDINATED,
485 day_count_convention="30E360",
486 business_day_convention="ModifiedFollowing",
487 margin=0.00,
488 frequency="1Y",
489 fixings=fix_table,
490 adjust_start_date=False,
491 adjust_end_date=False,
492 adjust_schedule=False,
493 adjust_accruals=False,
494 )
495 self.assertAlmostEqual(bpricer.get_accrued_interest(float_bond_spec_2, ref_date, dc), 1.666666666666, delta=1e-6)
497 def test_index_float_bond(self):
498 ref_date = datetime(2023, 5, 1)
499 fix_table = FixingTable("PV_BOND")
500 fix_table.add("EURIBOR 12M", datetime(2022, 12, 30), 0.05)
501 float_index_bond_spec = FloatingRateBondSpecification(
502 "PV_BOND",
503 issue_date=datetime(2023, 1, 1),
504 maturity_date=datetime(2025, 1, 2),
505 currency=Currency.EUR,
506 notional=100.0,
507 issuer="None",
508 securitization_level=SecuritizationLevel.SUBORDINATED,
509 day_count_convention="ACTACTICMA",
510 business_day_convention="ModifiedFollowing",
511 margin=0.00,
512 index="EURIBOR_1Y",
513 frequency="1Y",
514 fixings=fix_table,
515 adjust_accruals=False,
516 adjust_schedule=False,
517 )
518 self.assertEqual(float_index_bond_spec._index, "EURIBOR_1Y")
519 self.assertEqual(float_index_bond_spec._ir_index.value.name, "EURIBOR 12M")
520 dc = DiscountCurveParametrized("", ref_date, ConstantRate(0.05))
521 nr_annual_payments = float_index_bond_spec.get_nr_annual_payments()
522 self.assertEqual(nr_annual_payments, 1)
523 dates = float_index_bond_spec.get_schedule()._roll_out(
524 from_=float_index_bond_spec._start_date if not float_index_bond_spec._backwards else float_index_bond_spec._end_date,
525 to_=float_index_bond_spec._end_date if not float_index_bond_spec._backwards else float_index_bond_spec._start_date,
526 term=_term_to_period(float_index_bond_spec._frequency),
527 long_stub=float_index_bond_spec._stub_type_is_Long,
528 backwards=float_index_bond_spec._backwards,
529 )
530 ## discount factors
532 df1 = dc.value(ref_date, datetime(2024, 1, 2))
533 df2 = dc.value(ref_date, datetime(2025, 1, 2))
534 dcc = DayCounter(float_index_bond_spec.day_count_convention)
535 self.assertAlmostEqual(df1, 0.9668628440577077, delta=1e-6)
536 self.assertAlmostEqual(df2, 0.9195824079027841, delta=1e-6)
537 price = df1 * 5.0 + df2 * 105.0
538 self.assertEqual(dates, [datetime(2023, 1, 2), datetime(2024, 1, 2), datetime(2025, 1, 2)])
539 expected = [
540 (datetime(2023, 1, 2), -100.0),
541 (datetime(2024, 1, 2), 5.0),
542 (datetime(2025, 1, 2), 5.0),
543 (datetime(2025, 1, 2), 100.0),
544 ]
545 actual = DeterministicCashflowPricer.get_expected_cashflows(float_index_bond_spec, ref_date, dc)
547 self.assertEqual(len(actual), len(expected))
548 for i, (exp, act) in enumerate(zip(expected, actual)):
549 with self.subTest(i=i):
550 # exact equality for the date
551 self.assertEqual(exp[0], act[0])
552 # almost-equal for the amount with a tolerance (choose places or delta)
553 self.assertAlmostEqual(exp[1], act[1], places=6)
554 self.assertAlmostEqual(DeterministicCashflowPricer.get_pv_cashflows(ref_date, float_index_bond_spec, dc, dc), price, delta=1e-6)
555 bond_yield = DeterministicCashflowPricer.get_compute_yield(
556 target_dirty_price=price, val_date=ref_date, specification=float_index_bond_spec, fwd_curve=dc
557 )
558 self.assertAlmostEqual(bond_yield, 0.05, delta=1e-3)
559 bpricer = DeterministicCashflowPricer(ref_date, float_index_bond_spec, dc, dc)
560 self.assertAlmostEqual(bpricer.compute_yield(price), 0.05, delta=1e-3)
561 mc_duration = (
562 dcc.yf(ref_date, datetime(2024, 1, 2), float_index_bond_spec.dates, float_index_bond_spec.nr_annual_payments) * df1 * 5.0 / price
563 + dcc.yf(ref_date, datetime(2025, 1, 2), float_index_bond_spec.dates, float_index_bond_spec.nr_annual_payments) * df2 * 105.0 / price
564 )
565 self.assertAlmostEqual(bpricer.macaulay_duration(), mc_duration, delta=1e-3)
566 self.assertAlmostEqual(bpricer.z_spread(price), 0.0, delta=1e-6)
567 self.assertAlmostEqual(bpricer.macaulay_duration(), mc_duration, delta=1e-3)
568 self.assertAlmostEqual(bpricer.modified_duration(price), mc_duration / 1.05, delta=1e-3)
570 def test_amort_bond(self):
571 """Simple test for yield computation: Fixed rate used to compute pv which is then used to compute yield."""
572 ref_date = datetime(2023, 5, 1)
573 bond_amort_spec = FixedRateBondSpecification(
574 "PV_BOND",
575 issue_date=datetime(2023, 1, 1),
576 maturity_date=datetime(2025, 1, 2),
577 currency=Currency.EUR,
578 notional=100.0,
579 amortization_scheme="linear",
580 issuer="None",
581 securitization_level=SecuritizationLevel.SUBORDINATED,
582 coupon=0.05,
583 frequency="1Y",
584 )
585 dc = DiscountCurveParametrized("", ref_date, ConstantRate(0.05))
586 nr_annual_payments = bond_amort_spec.get_nr_annual_payments()
587 self.assertEqual(nr_annual_payments, 1)
588 # schedule for accrual periods rolled out
589 dates = bond_amort_spec.get_schedule()._roll_out(
590 from_=bond_amort_spec._start_date if not bond_amort_spec._backwards else bond_amort_spec._end_date,
591 to_=bond_amort_spec._end_date if not bond_amort_spec._backwards else bond_amort_spec._start_date,
592 term=_term_to_period(bond_amort_spec._frequency),
593 long_stub=bond_amort_spec._stub_type_is_Long,
594 backwards=bond_amort_spec._backwards,
595 )
596 # print(bond_amort_spec._notional.notional)
598 ## discount factors
600 df1 = dc.value(ref_date, datetime(2024, 1, 2))
601 df2 = dc.value(ref_date, datetime(2025, 1, 2))
602 dcc = DayCounter(bond_amort_spec.day_count_convention)
603 self.assertAlmostEqual(df1, 0.9668628440577077, delta=1e-6)
604 self.assertAlmostEqual(df2, 0.9195824079027841, delta=1e-6)
605 price = df1 * 55.0 + df2 * 52.5
606 self.assertAlmostEqual(DeterministicCashflowPricer.get_pv_cashflows(ref_date, bond_amort_spec, dc), price, delta=1e-6)
608 self.assertEqual(dates, [datetime(2023, 1, 2), datetime(2024, 1, 2), datetime(2025, 1, 2)])
609 # print(DeterministicCashflowPricer.get_expected_cashflows(bond_amort_spec))
610 self.assertEqual(
611 DeterministicCashflowPricer.get_expected_cashflows(bond_amort_spec),
612 [
613 (datetime(2023, 1, 2), -100.0),
614 (datetime(2024, 1, 2), 5.0),
615 (datetime(2024, 1, 2), 50.0),
616 (datetime(2025, 1, 2), 2.5),
617 (datetime(2025, 1, 2), 50.0),
618 ],
619 )
620 bond_price = DeterministicCashflowPricer.get_pv_cashflows(ref_date, bond_amort_spec, dc)
621 bond_yield = DeterministicCashflowPricer.get_compute_yield(target_dirty_price=bond_price, val_date=ref_date, specification=bond_amort_spec)
622 self.assertAlmostEqual(bond_yield, 0.05, delta=1e-3)
623 bpricer = DeterministicCashflowPricer(ref_date, bond_amort_spec, dc)
624 self.assertAlmostEqual(bpricer.compute_yield(bond_price), 0.05, delta=1e-3)
625 mc_duration = (
626 dcc.yf(ref_date, datetime(2024, 1, 2), bond_amort_spec.dates, bond_amort_spec.nr_annual_payments) * df1 * 55.0 / bond_price
627 + dcc.yf(ref_date, datetime(2025, 1, 2), bond_amort_spec.dates, bond_amort_spec.nr_annual_payments) * df2 * 52.5 / bond_price
628 )
629 self.assertAlmostEqual(bpricer.macaulay_duration(), mc_duration, delta=1e-3)
630 self.assertAlmostEqual(bpricer.z_spread(bond_price), 0.0, delta=1e-6)
631 self.assertAlmostEqual(bpricer.macaulay_duration(), mc_duration, delta=1e-3)
632 self.assertAlmostEqual(bpricer.modified_duration(bond_price), mc_duration / 1.05, delta=1e-3)
635if __name__ == "__main__":
636 main()