Coverage for tests / test_interpolate.py: 99%
141 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 numpy as np
3from rivapy.tools.interpolate import Interpolator
4from rivapy.tools.enums import InterpolationType, ExtrapolationType
7# , delta=1e-5 ?
8class TestInterpolator(unittest.TestCase):
10 def setUp(self):
11 """Test data, simple linear case. Extend to more robust if requested."""
12 self.x = [0, 1, 2, 3]
13 self.y = [0, 10, 20, 30]
15 def test_linear_interpolation_scalar(self):
16 """Test single target x argument"""
17 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.LINEAR)
18 result = interpolator.interp(self.x, self.y, 1.5, "LINEAR")
19 self.assertAlmostEqual(result, 15.0)
21 def test_linear_interpolation_list(self):
22 """Test multi target x arguments"""
23 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.LINEAR)
24 result = interpolator.interp(self.x, self.y, [0.5, 1.5, 2.5], "LINEAR")
25 expected = [5.0, 15.0, 25.0]
26 self.assertEqual(len(result), len(expected))
27 for r, e in zip(result, expected):
28 self.assertAlmostEqual(r, e)
30 def test_linear_extrapolation_left_right(self):
31 """Test both edges of extrapolation."""
32 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.LINEAR)
33 result = interpolator.interp(self.x, self.y, -1, "LINEAR")
34 expected = -10.0 # slope = 10/unit, so -1 → -10 from given self.x and self.y
35 self.assertAlmostEqual(result, expected)
37 result = interpolator.interp(self.x, self.y, 4, "LINEAR")
38 expected = 40
39 self.assertAlmostEqual(result, expected)
41 def test_constant_extrapolation(self):
42 """Test the case of CONSTANT extrapolation mode selected."""
43 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.CONSTANT)
44 result_left = interpolator.interp(self.x, self.y, -10, "CONSTANT")
45 result_right = interpolator.interp(self.x, self.y, 10, "CONSTANT")
46 self.assertEqual(result_left, self.y[0])
47 self.assertEqual(result_right, self.y[-1])
49 def test_no_extrapolation_raises(self):
50 """Test for the case that extrapolation was set to NONE but target values
51 are outside data range
52 """
53 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.NONE)
54 with self.assertRaises(ValueError):
55 interpolator.interp(self.x, self.y, -1, "NONE")
56 with self.assertRaises(ValueError):
57 interpolator.interp(self.x, self.y, 4, "NONE")
59 def test_no_extrapolationType_raises(self):
60 """Test for the case that extrapolation was set to NONE and the extrapolation
61 argument given is ExtrapolationType and not a str
62 """
63 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.NONE)
64 with self.assertRaises(ValueError):
65 interpolator.interp(self.x, self.y, -1, ExtrapolationType.NONE)
66 with self.assertRaises(ValueError):
67 interpolator.interp(self.x, self.y, 4, ExtrapolationType.NONE)
69 def test_mismatched_length_raises(self):
70 """Test the case of incorrect data input length mismatch."""
71 interpolator = Interpolator(InterpolationType.LINEAR, ExtrapolationType.LINEAR)
72 with self.assertRaises(ValueError):
73 interpolator.interp([0, 1, 2], [10, 20], 1.5, "LINEAR")
76class TestLinearLogInterpolator(unittest.TestCase):
78 def setUp(self):
79 """Set up sample positive data suitable for log-linear interpolation."""
80 self.x = [0.0, 1.0, 2.0, 3.0]
81 # discount factors that decline exponentially — log-linear interpolation should reproduce this exactly
82 self.df = [1.0, np.exp(-0.02), np.exp(-0.04), np.exp(-0.06)] # constant rate 2% per year
84 def test_log_linear_exact_exponential(self):
85 """
86 For exponentially decaying data, log-linear interpolation should be exact.
87 DF(x) = exp(-0.02 * x)
88 """
89 interpolator = Interpolator(InterpolationType.LINEAR_LOG, ExtrapolationType.LINEAR)
90 for x in [0.5, 1.5, 2.5]:
91 df_interp = interpolator.interp(self.x, self.df, x, "LINEAR_LOG")
92 expected = np.exp(-0.02 * x)
93 self.assertAlmostEqual(df_interp, expected, delta=1e-12)
95 def test_log_linear_vector_input(self):
96 """Test list input of target x values produces correct list output."""
97 interpolator = Interpolator(InterpolationType.LINEAR_LOG, ExtrapolationType.LINEAR)
98 x_targets = [0.5, 1.0, 2.5]
99 result = interpolator.interp(self.x, self.df, x_targets, "LINEAR_LOG")
100 expected = [np.exp(-0.02 * x) for x in x_targets]
101 self.assertEqual(len(result), len(expected))
102 for r, e in zip(result, expected):
103 self.assertAlmostEqual(r, e, delta=1e-12)
105 def test_log_linear_extrapolation_linear_mode(self):
106 """Check extrapolation using LINEAR mode reproduces reasonable continuation."""
107 interpolator = Interpolator(InterpolationType.LINEAR_LOG, ExtrapolationType.LINEAR)
108 x_extrap = 4.0
109 df_interp = interpolator.interp(self.x, self.df, x_extrap, "LINEAR_LOG")
110 expected = np.exp(-0.02 * 4.0)
111 self.assertAlmostEqual(df_interp, expected, delta=1e-12)
113 def test_log_linear_requires_positive_y(self):
114 """Log-linear interpolation must raise an error if any y <= 0."""
115 interpolator = Interpolator(InterpolationType.LINEAR_LOG, ExtrapolationType.LINEAR)
116 bad_y = [1.0, 0.5, 0.0, -0.5]
117 with self.assertRaises(ValueError):
118 interpolator.interp(self.x, bad_y, 1.0, "LINEAR_LOG")
120 def test_log_linear_constant_extrapolation(self):
121 """Verify constant extrapolation on left/right boundaries."""
122 interpolator = Interpolator(InterpolationType.LINEAR_LOG, ExtrapolationType.CONSTANT)
123 result_left = interpolator.interp(self.x, self.df, -1.0, "CONSTANT")
124 result_right = interpolator.interp(self.x, self.df, 4.0, "CONSTANT")
125 self.assertAlmostEqual(result_left, self.df[0], delta=1e-12)
126 self.assertAlmostEqual(result_right, self.df[-1], delta=1e-12)
128class TestHaganInterpolator(unittest.TestCase):
130 def setUp(self):
131 # Simple synthetic data — exponential discount curve
132 # DF(x) = exp(-r*x), with r = 0.05 constant forward rate
133 self.x = [0.0, 1.0, 2.0, 3.0, 5.0]
134 self.r = 0.05
135 self.df = [np.exp(-self.r * t) for t in self.x]
137 # Forward rates between grid points should all be ~0.05
138 self.fwd = (np.log(self.df[:-1]) - np.log(self.df[1:])) / (np.diff(self.x))
141 def test_hagan_polynomials_shapes(self):
142 """Ensure polynomial coefficient arrays have consistent lengths.
143 """
144 x_vals, a0, a1, a2 = Interpolator._hagan_polynomials(self.x, self.fwd)
145 self.assertEqual(len(a0), len(a1))
146 self.assertEqual(len(a1), len(a2))
147 self.assertTrue(len(x_vals) >= 2)
150 def test_hagan_constant_forward_rate(self):
151 """Flat forward rate curve -> all interpolated points must equal 0.05.
152 """
153 for x in np.linspace(0.1, 4.9, 9):
154 f_interp = Interpolator.hagan(self.x, self.fwd, x, "CONSTANT")
155 self.assertAlmostEqual(f_interp, self.r, delta=1e-10)
158 def test_hagan_integrate_matches_analytical(self):
159 """
160 For constant forward rate = 0.05, integral from 0→x should be 0.05 * x.
161 """
162 for x in [0.5, 1.0, 2.5, 4.0]:
163 integral = Interpolator.hagan_integrate(self.x, self.fwd, x)
164 expected = self.r * x
165 self.assertAlmostEqual(integral, expected, delta=1e-10)
168 def test_hagan_df_inside_grid(self):
169 """Inside grid, DF(x) = exp(-0.05 * x).
170 """
171 for x in [0.5, 1.0, 2.0, 3.5]:
172 df_interp = Interpolator.hagan_df(self.x, self.df, x, "CONSTANT_DF")
173 expected = np.exp(-self.r * x)
174 self.assertAlmostEqual(df_interp, expected, delta=1e-10)
177 def test_hagan_df_extrapolation_constant_df(self):
178 """Extrapolated DF should follow exp(-r_avg * x) rule.
179 """
180 # Compute "average rate" up to last point
181 y = Interpolator.hagan_integrate(self.x, self.fwd, self.x[-1])
182 r_avg = y / self.x[-1]
184 for x in [6.0, 7.5, 10.0]:
185 df_extrap = Interpolator.hagan_df(self.x, self.df, x, "CONSTANT_DF")
186 expected = np.exp(-r_avg * x)
187 self.assertAlmostEqual(df_extrap, expected, delta=1e-10)
189 def test_hagan_df_extrapolation_below_first(self):
190 """Check extrapolation below grid for CONSTANT_DF."""
191 y = Interpolator.hagan_integrate(self.x, self.fwd, self.x[1])
192 r_avg = y / self.x[1]
194 for x in [-0.5, -1.0]:
195 df_extrap = Interpolator.hagan_df(self.x, self.df, x, "CONSTANT_DF")
196 expected = np.exp(-r_avg * x)
197 self.assertAlmostEqual(df_extrap, expected, delta=1e-10)
200 def test_hagan_df_derivative_relation(self):
201 """
202 DF'(x) = -f(x) * DF(x). Test numerically for constant forward rate.
203 """
204 for x in [0.5, 1.0, 2.5, 4.0]:
205 df_interp = Interpolator.hagan_df(self.x, self.df, x, "CONSTANT_DF")
206 f_interp = Interpolator.hagan(self.x, self.fwd, x, "CONSTANT_DF")
207 df_prime = Interpolator.hagan_df_derivative(self.x, self.df, x, "CONSTANT_DF")
209 expected = -f_interp * df_interp
210 self.assertAlmostEqual(df_prime, expected, delta=1e-10)
212 # --------------------------
213 # Error cases
214 def test_hagan_df_raises_for_short_input(self):
215 """Require at least 2 discount factors."""
216 with self.assertRaises(ValueError):
217 Interpolator.hagan_df([1.0], [0.99], 0.5, "CONSTANT_DF")
219 def test_hagan_extrapolation_raises_for_none(self):
220 """Extrapolation NONE should raise."""
221 with self.assertRaises(ValueError):
222 Interpolator.hagan_df(self.x, self.df, 10.0, "NONE")
224 def test_hagan_forward_out_of_bounds_raises(self):
225 """Extrapolation type not allowed for forward interpolation."""
226 with self.assertRaises(ValueError):
227 Interpolator.hagan(self.x, self.fwd, -1.0, "NONE")
231if __name__ == "__main__":
232 unittest.main()