Coverage for tests / test_fras.py: 99%
97 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
1# hnguyen, 2024-09-08
2# unit tests for Forward rate agreement specification class
3import math
4from rivapy.tools.holidays_compat import ECB # ??
6import unittest
7import datetime as dt
8from matplotlib import dates
9import numpy as np
10from rivapy.instruments.fra_specifications import ForwardRateAgreementSpecification
11from rivapy.marketdata.curves import DiscountCurve
12from rivapy.pricing.fra_pricing import ForwardRateAgreementPricer
13from rivapy.pricing.bond_pricing import DeterministicCashflowPricer
14from rivapy.tools.datetools import DayCounter, roll_day
15from rivapy.tools.enums import (
16 DayCounterType,
17 InterpolationType,
18 RollConvention,
19 SecuritizationLevel,
20 Currency,
21 Rating,
22 Instrument,
23)
26class TestForwardRateAgreementSpecification(unittest.TestCase):
28 # Set up FRA
29 ccy = "EUR"
30 fra_rate = 0.04
31 ref_date = dt.datetime(2023, 1, 28)
32 start_date = dt.datetime(2023, 7, 28)
33 end_date = dt.datetime(2023, 10, 28)
35 mat_date = ref_date + dt.timedelta(days=365)
37 fra = ForwardRateAgreementSpecification(
38 obj_id="dummy_id",
39 trade_date=ref_date,
40 notional=1000.0,
41 rate=fra_rate,
42 start_date=start_date,
43 end_date=end_date,
44 udlID="dummy_underlying_index",
45 rate_start_date=start_date,
46 rate_end_date=end_date,
47 day_count_convention="Act360",
48 rate_day_count_convention="Act360",
49 currency=ccy,
50 spot_days=1,
51 payment_days=1,
52 issuer="dummy_issuer",
53 securitization_level="NONE",
54 )
56 def setUp(self):
57 """Common setup for tests"""
58 self.trade_date = dt.date(2024, 1, 1)
59 self.maturity_date = dt.date(2024, 12, 31)
60 self.start_date = dt.date(2024, 3, 1)
61 self.end_date = dt.date(2024, 9, 1)
63 self.fra = ForwardRateAgreementSpecification(
64 obj_id="fra_001",
65 trade_date=self.trade_date,
66 maturity_date=self.maturity_date,
67 notional=1_000_000,
68 rate=0.02,
69 start_date=self.start_date,
70 end_date=self.end_date,
71 udlID="EURIBOR_3M", # at the moment, dummy, not used in pricing YET
72 rate_start_date=self.start_date - dt.timedelta(days=2),
73 rate_end_date=self.end_date - dt.timedelta(days=2),
74 calendar=ECB(years=range(2024, 2025)),
75 currency="EUR",
76 issuer="TestBank",
77 securitization_level=SecuritizationLevel.NONE,
78 rating=Rating.A,
79 payment_days=2,
80 )
82 def test_initialization(self):
83 self.assertEqual(self.fra.obj_id, "fra_001")
84 self.assertEqual(self.fra.notional, 1_000_000)
85 self.assertEqual(self.fra.rate, 0.02)
86 self.assertEqual(self.fra.trade_date, self.trade_date)
87 self.assertEqual(self.fra.maturity_date, self.maturity_date)
88 self.assertEqual(self.fra.start_date, self.start_date)
89 self.assertEqual(self.fra.end_date, self.end_date)
90 self.assertEqual(self.fra.currency, "EUR")
91 self.assertEqual(self.fra.issuer, "TestBank")
92 self.assertEqual(self.fra.securitization_level, "NONE")
93 self.assertEqual(self.fra.rating, "A")
94 self.assertEqual(self.fra.payment_days, 2)
96 def test_to_dict(self):
97 d = self.fra._to_dict()
98 self.assertIsInstance(d, dict)
99 self.assertEqual(d["obj_id"], "fra_001")
100 self.assertEqual(d["currency"], "EUR")
101 self.assertEqual(d["notional"], 1_000_000)
103 def test_property_setters(self):
104 self.fra.rate = 0.05
105 self.assertEqual(self.fra.rate, 0.05)
107 self.fra.notional = 5000
108 self.assertEqual(self.fra.notional, 5000)
110 self.fra.issuer = "NewBank"
111 self.assertEqual(self.fra.issuer, "NewBank")
113 self.fra.rating = Rating.B
114 self.assertEqual(self.fra.rating, "B")
116 self.fra.currency = Currency.USD # should be fine despite the warning
117 self.assertEqual(self.fra.currency, "USD")
119 def test_invalid_notional_raises(self):
120 with self.assertRaises(ValueError):
121 self.fra.notional = -1000
123 def test_ins_type(self):
124 self.assertEqual(self.fra.ins_type(), Instrument.FRA)
126 def test_get_end_date_alias(self):
127 self.assertEqual(self.fra.get_end_date(), self.maturity_date)
129 def test_create_sample_reproducibility(self):
130 samples1 = ForwardRateAgreementSpecification._create_sample(3, seed=42, ref_date=self.trade_date)
131 samples2 = ForwardRateAgreementSpecification._create_sample(3, seed=42, ref_date=self.trade_date)
133 self.assertEqual(len(samples1), 3)
134 self.assertEqual(len(samples2), 3)
135 # Same seed → same first instrument spec
136 # self.assertEqual(samples1[0]["notional"], samples2[0]["notional"])
137 # self.assertEqual(samples1[0]["currency"], samples2[0]["currency"])
139 # def test_create_sample_structure(self):
140 # samples = ForwardRateAgreementSpecification._create_sample(2, seed=1, ref_date=self.trade_date)
141 # self.assertIsInstance(samples, list)
142 # self.assertIn("trade_date", samples[0])
143 # self.assertIn("maturity_date", samples[0])
144 # self.assertIn("currency", samples[0])
146 #######################################################
147 # Tests for Pricing
148 def test_fra_cf_implied_rate(self):
149 # setting up necessary curves
150 # discount curve
151 object_id = "TEST_DC"
152 dsc_rate = 0.01
153 days_to_maturity = [1, 180, 365, 720, 3 * 365, 4 * 365, 10 * 365]
154 dates = [self.ref_date + dt.timedelta(days=d) for d in days_to_maturity]
155 df = [math.exp(-d / 365.0 * dsc_rate) for d in days_to_maturity]
156 dc = DiscountCurve(id=object_id, refdate=self.ref_date, dates=dates, df=df, interpolation=InterpolationType.LINEAR)
158 # Fixing curve
159 object_id = "TEST_fwd"
160 fwd_rate = 0.05
161 fwd_df = [math.exp(-d / 365.0 * fwd_rate) for d in days_to_maturity]
162 fwd_dc = DiscountCurve(id=object_id, refdate=self.ref_date, dates=dates, df=fwd_df, interpolation=InterpolationType.LINEAR)
164 fra_pricer = ForwardRateAgreementPricer(self.ref_date, self.fra, dc, fwd_dc)
166 # Manually calculate expected cashflows 'manually' for comparison
167 dcc_rate = DayCounter(fwd_dc.daycounter)
168 fwdrateDF = fwd_dc.value_fwd(self.ref_date, self.fra._rate_start_date, self.fra._rate_end_date)
169 dt_rate = dcc_rate.yf(self.fra._rate_start_date, self.fra._rate_end_date)
170 fwdrate = (1.0 / fwdrateDF - 1) / dt_rate
172 # using instrument daycount convention to calculate delta t for cf amount calculation and discounting
173 dcc = DayCounter(self.fra.day_count_convention)
174 # avoid shadowing the datetime module alias `dt` imported at module level
175 # (assignment to `dt` would make `dt` local in this function and cause
176 # UnboundLocalError when `dt.timedelta` is used earlier)
177 year_frac = dcc.yf(self.fra._start_date, self.fra._end_date)
178 amount = self.fra._notional * (fwdrate - self.fra._rate) * year_frac
179 cf = amount / (1 + fwdrate * year_frac)
180 fair_rate = (1.0 / fwdrateDF - 1) / dt_rate
181 self.assertEqual(fra_pricer._fra_spec, self.fra)
182 self.assertEqual(fra_pricer._val_date, self.ref_date)
183 self.assertEqual(fra_pricer._discount_curve, dc)
184 self.assertEqual(fra_pricer._forward_curve, fwd_dc)
186 self.assertEqual(
187 fra_pricer.get_expected_cashflows(self.fra, self.ref_date, fwd_dc),
188 [(roll_day(self.fra._start_date, self.fra._calendar, self.fra._business_day_convention, settle_days=self.fra._payment_days), cf)],
189 )
190 self.assertEqual(fra_pricer.compute_fair_rate(self.ref_date, self.fra, fwd_dc), fair_rate)
193if __name__ == "__main__":
194 unittest.main()