Coverage for tests / test_curves.py: 99%
117 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
1import unittest
2import datetime as dt
3import math
5from rivapy.tools.interpolate import Interpolator
6from rivapy.tools.enums import DayCounterType, InterpolationType, ExtrapolationType
7from rivapy.marketdata import DiscountCurve, SurvivalCurve, EquityForwardCurve
8from rivapy.tools.datetools import DayCounter
11# , delta=1e-5 ?
12class TestDiscountCurve(unittest.TestCase):
14 # Discount Curve has
15 # _init_
16 # get_dates
17 # get_df
18 # value
19 # get_pyvacon_obj
20 # value - cares about daycount convention refdates, target date, interpolation type, extrapolation type..
21 # at the moment, it is assumed the target date is correctly calculated before input with correct business day logic/roll convention
22 # there is at the moment a potential issue with the roll convention and the given reference date, for now assume it is correct #TODO
23 # plot - calls the value or value in order to plot.
24 # TODO: consider how to wrap both value and value depending on if the interpolationType is a pyvacon or Rivapy construction respectively
26 def setUp(self):
27 """Test data, simple linear case. Extend to more robust if requested."""
29 # Example discount curve, with discount factors calculated based on given rates and days to maturity
30 # first example uses day count convention ACT365FIXED
32 # base imformation
33 self.refdate = dt.datetime(2017, 1, 1, 0, 0, 0)
34 self.days_to_maturity = [1, 180, 365, 720, 3 * 365, 4 * 365, 10 * 365]
35 self.rates = [-0.0065, 0.0003, 0.0059, 0.0086, 0.0101, 0.02, 0.03]
36 self.rate = 0.03
37 self.dates = [self.refdate + dt.timedelta(days=i) for i in self.days_to_maturity]
39 self.dsc_fac_ACT365FIXED = [math.exp(-d / 365.0 * self.rate) for d in self.days_to_maturity]
41 def test_init_success_and_getters(self):
42 """Check initialization and getters."""
43 dc = DiscountCurve(
44 "test_curve",
45 self.refdate,
46 self.dates,
47 self.dsc_fac_ACT365FIXED,
48 InterpolationType.LINEAR,
49 ExtrapolationType.LINEAR,
50 DayCounterType.Act365Fixed,
51 )
52 self.assertEqual(dc.id, "test_curve")
53 self.assertEqual(dc.get_dates()[0], self.refdate)
54 self.assertEqual(dc.get_df()[0], 1.0)
55 self.assertTrue(all(isinstance(d, dt.datetime) for d in dc.get_dates()))
57 def test_init_invalid_inputs(self):
58 """Invalid input combinations must raise where appropriate."""
60 # Empty dates and dfs — must raise
61 with self.assertRaises(Exception):
62 DiscountCurve("x", self.refdate, [], [])
64 # Length mismatch
65 with self.assertRaises(Exception):
66 DiscountCurve("x", self.refdate, [self.refdate + dt.timedelta(days=1)], [0.9, 0.8])
68 # Non-enum arguments
69 with self.assertRaises(TypeError):
70 DiscountCurve(
71 "x",
72 self.refdate,
73 [self.refdate + dt.timedelta(days=1)],
74 [1.0],
75 interpolation="BAD",
76 extrapolation=ExtrapolationType.LINEAR,
77 daycounter=DayCounterType.Act365Fixed,
78 )
79 with self.assertRaises(TypeError):
80 DiscountCurve(
81 "x",
82 self.refdate,
83 [self.refdate + dt.timedelta(days=1)],
84 [1.0],
85 interpolation=InterpolationType.LINEAR,
86 extrapolation="BAD",
87 daycounter=DayCounterType.Act365Fixed,
88 )
89 with self.assertRaises(TypeError):
90 DiscountCurve(
91 "x",
92 self.refdate,
93 [self.refdate + dt.timedelta(days=1)],
94 [1.0],
95 interpolation=InterpolationType.LINEAR,
96 extrapolation=ExtrapolationType.LINEAR,
97 daycounter="BAD",
98 )
100 # First date before refdate
101 with self.assertRaises(Exception):
102 DiscountCurve(
103 "x",
104 self.refdate,
105 [self.refdate - dt.timedelta(days=1)],
106 [1.0],
107 InterpolationType.LINEAR,
108 ExtrapolationType.LINEAR,
109 DayCounterType.Act365Fixed,
110 )
112 # Instead of expecting an exception, assert correct behavior:
113 dc = DiscountCurve(
114 "x",
115 self.refdate,
116 [self.refdate + dt.timedelta(days=1)],
117 [0.9],
118 InterpolationType.LINEAR,
119 ExtrapolationType.LINEAR,
120 DayCounterType.Act365Fixed,
121 )
122 # constructor should have prepended (refdate, 1.0)
123 dates = dc.get_dates()
124 dfs = dc.get_df()
125 self.assertEqual(dates[0], self.refdate)
126 self.assertEqual(dfs[0], 1.0)
127 self.assertEqual(dates[1], self.refdate + dt.timedelta(days=1))
128 self.assertAlmostEqual(dfs[1], 0.9)
130 # Non-monotonic or duplicate dates
131 with self.assertRaises(Exception):
132 DiscountCurve(
133 "x",
134 self.refdate,
135 [self.refdate + dt.timedelta(days=1), self.refdate + dt.timedelta(days=1)],
136 [1.0, 0.99],
137 InterpolationType.LINEAR,
138 ExtrapolationType.LINEAR,
139 DayCounterType.Act365Fixed,
140 )
142 def test_get_dates_and_get_df(self):
143 dc = DiscountCurve(
144 "Test_DC_ACT365FIXED",
145 self.refdate,
146 self.dates,
147 self.dsc_fac_ACT365FIXED,
148 InterpolationType.LINEAR,
149 ExtrapolationType.LINEAR,
150 DayCounterType.Act365Fixed,
151 )
152 dates = dc.get_dates()
153 dfs = dc.get_df()
154 self.assertEqual(len(dates), len(dfs))
155 self.assertEqual(dates[0], self.refdate)
156 self.assertEqual(dfs[0], 1.0)
158 def test_value_and_extrapolation_cases(self):
159 """Replicates and extends original test_value logic."""
160 dc_linear = DiscountCurve(
161 "Test_DC_ACT365FIXED",
162 self.refdate,
163 self.dates,
164 self.dsc_fac_ACT365FIXED,
165 InterpolationType.LINEAR,
166 ExtrapolationType.LINEAR,
167 DayCounterType.Act365Fixed,
168 )
170 df1 = dc_linear.value(self.refdate, self.refdate + dt.timedelta(days=90))
171 df2 = dc_linear.value(self.refdate, self.refdate + dt.timedelta(days=180))
172 fwd_df = dc_linear.value(self.refdate + dt.timedelta(days=90), self.refdate + dt.timedelta(days=180))
173 self.assertAlmostEqual(df1, 0.9926568878362608, delta=1e-5)
174 self.assertAlmostEqual(df2, 0.9853143806626516, delta=1e-5)
175 self.assertAlmostEqual(fwd_df, df2 / df1, delta=1e-5)
177 # Linear extrapolation
178 df_extrap1 = dc_linear.value(self.refdate, self.refdate + dt.timedelta(days=10 * 365 + 10))
179 df_extrap2 = dc_linear.value(self.refdate, self.refdate + dt.timedelta(days=10 * 365 + 60))
180 self.assertAlmostEqual(df_extrap1, 0.7401510872751634, delta=1e-5)
181 self.assertAlmostEqual(df_extrap2, 0.7368154202423907, delta=1e-5)
183 # Constant extrapolation
184 dc_const = DiscountCurve(
185 "Test_DC_ACT365FIXED",
186 self.refdate,
187 self.dates,
188 self.dsc_fac_ACT365FIXED,
189 InterpolationType.LINEAR,
190 ExtrapolationType.CONSTANT,
191 DayCounterType.Act365Fixed,
192 )
193 df_const1 = dc_const.value(self.refdate, self.refdate + dt.timedelta(days=10 * 365 + 10))
194 df_const2 = dc_const.value(self.refdate, self.refdate + dt.timedelta(days=10 * 365 + 60))
195 self.assertAlmostEqual(df_const1, df_const2, delta=1e-10)
197 # Extrapolation NONE -> should raise ValueError
198 dc_none = DiscountCurve(
199 "Test_DC_ACT365FIXED",
200 self.refdate,
201 self.dates,
202 self.dsc_fac_ACT365FIXED,
203 InterpolationType.LINEAR,
204 ExtrapolationType.NONE,
205 DayCounterType.Act365Fixed,
206 )
207 with self.assertRaises(ValueError):
208 dc_none.value(self.refdate, self.refdate + dt.timedelta(days=4000))
210 # TODO:
211 # Tests with other DCC
213 # ActAct, LINEAR, LINEAR
214 # ActAct, LINEAR, CONSTANT
215 # ActAct, LINEAR, NONE
216 # Act360, LINEAR, LINEAR
217 # Act360, LINEAR, CONSTANT
218 # Act360, LINEAR, NONE
219 # 30U360, LINEAR, LINEAR
220 # 30U360, LINEAR, CONSTANT
221 # 30U360, LINEAR, NONE
223 # TODO: TEST LIST of given dates
225 def test_value_rate_and_yf(self):
226 dc = DiscountCurve(
227 "Test_DC_ACT365FIXED",
228 self.refdate,
229 self.dates,
230 self.dsc_fac_ACT365FIXED,
231 InterpolationType.LINEAR,
232 ExtrapolationType.LINEAR,
233 DayCounterType.Act365Fixed,
234 )
235 d = self.refdate + dt.timedelta(days=365)
236 rate = dc.value_rate(self.refdate, d)
237 df = dc.value(self.refdate, d)
238 expected = -math.log(df) / DayCounter(DayCounterType.Act365Fixed).yf(self.refdate, d)
239 self.assertAlmostEqual(rate, expected, delta=1e-12)
241 df_yf = dc.value_yf(0.5)
242 self.assertIsInstance(df_yf, float)
244 def test_value_fwd_and_fwd_rate(self):
245 dc = DiscountCurve(
246 "Test_DC_ACT365FIXED",
247 self.refdate,
248 self.dates,
249 self.dsc_fac_ACT365FIXED,
250 InterpolationType.LINEAR,
251 ExtrapolationType.LINEAR,
252 DayCounterType.Act365Fixed,
253 )
255 d1 = self.refdate + dt.timedelta(days=365)
256 d2 = self.refdate + dt.timedelta(days=730)
257 fwd_df = dc.value_fwd(self.refdate, d1, d2)
258 self.assertTrue(fwd_df < 1.0)
259 fwd_rate = dc.value_fwd_rate(self.refdate, d1, d2)
260 self.assertAlmostEqual(fwd_rate, -math.log(fwd_df) / DayCounter(DayCounterType.Act365Fixed).yf(d1, d2))
262 # Value date > refdate triggers rebasement logic
263 val_date = self.refdate + dt.timedelta(days=500)
264 fwd_df2 = dc.value_fwd(val_date, d1, d2)
265 self.assertIsInstance(fwd_df2, float)
267 # Value date before refdate -> should raise
268 with self.assertRaises(Exception):
269 dc.value_fwd(self.refdate - dt.timedelta(days=1), d1, d2)
271 def test_call_zero_rate_and_rate_for_dates(self):
272 dc = DiscountCurve(
273 "Test_DC_ACT365FIXED",
274 self.refdate,
275 self.dates,
276 self.dsc_fac_ACT365FIXED,
277 InterpolationType.LINEAR,
278 ExtrapolationType.LINEAR,
279 DayCounterType.Act365Fixed,
280 )
282 # Direct zero rate for year fraction
283 z = dc(0.5)
284 self.assertIsInstance(z, float)
285 self.assertGreater(z, 0)
287 # Zero rate for given date/refdate
288 d = self.refdate + dt.timedelta(days=365)
289 z2 = dc(0.5, self.refdate, d)
290 self.assertIsInstance(z2, float)
292 # --------------------- Error and edge case tests --------------------------
294 def test_value_invalid_ref_before_curve_ref(self):
295 dc = DiscountCurve(
296 "Test_DC_ACT365FIXED",
297 self.refdate,
298 self.dates,
299 self.dsc_fac_ACT365FIXED,
300 InterpolationType.LINEAR,
301 ExtrapolationType.LINEAR,
302 DayCounterType.Act365Fixed,
303 )
304 with self.assertRaises(Exception):
305 dc.value(self.refdate - dt.timedelta(days=1), self.refdate + dt.timedelta(days=1))
307 def test_value_rate_invalid_ref_before_curve_ref(self):
308 dc = DiscountCurve(
309 "Test_DC_ACT365FIXED",
310 self.refdate,
311 self.dates,
312 self.dsc_fac_ACT365FIXED,
313 InterpolationType.LINEAR,
314 ExtrapolationType.LINEAR,
315 DayCounterType.Act365Fixed,
316 )
317 with self.assertRaises(Exception):
318 dc.value_rate(self.refdate - dt.timedelta(days=1), self.refdate + dt.timedelta(days=1))
320 # --------------------- Placeholder for HAGAN and plot ---------------------
321 # TODO
322 def test_value_HAGAN(self):
323 """Basic smoke test for HAGAN interpolation type."""
324 dc = DiscountCurve(
325 "Test_DC_HAGAN",
326 self.refdate,
327 self.dates,
328 self.dsc_fac_ACT365FIXED,
329 InterpolationType.HAGAN_DF,
330 ExtrapolationType.LINEAR,
331 DayCounterType.Act365Fixed,
332 )
333 val = dc.value(self.refdate, self.refdate + dt.timedelta(days=180))
334 self.assertIsInstance(val, float)
336 def test_plot_value(self):
337 """Plot test stub (if implemented in module)."""
338 dc = DiscountCurve(
339 "Test_DC_PLOT",
340 self.refdate,
341 self.dates,
342 self.dsc_fac_ACT365FIXED,
343 InterpolationType.LINEAR,
344 ExtrapolationType.LINEAR,
345 DayCounterType.Act365Fixed,
346 )
347 # The class itself doesn’t define plot(), but if added, ensure it runs
348 if hasattr(dc, "plot"):
349 dc.plot() # Smoke test
352if __name__ == "__main__":
353 unittest.main()