convex_math/interpolation/
mod.rs1mod cubic_spline;
41mod flat_forward;
42mod linear;
43mod log_linear;
44mod monotone_convex;
45mod parametric;
46
47pub use cubic_spline::CubicSpline;
48pub use flat_forward::FlatForward;
49pub use linear::LinearInterpolator;
50pub use log_linear::LogLinearInterpolator;
51pub use monotone_convex::MonotoneConvex;
52pub use parametric::{NelsonSiegel, Svensson};
53
54use crate::error::MathResult;
55
56pub trait Interpolator: Send + Sync {
61 fn interpolate(&self, x: f64) -> MathResult<f64>;
63
64 fn derivative(&self, x: f64) -> MathResult<f64>;
68
69 fn allows_extrapolation(&self) -> bool {
71 false
72 }
73
74 fn min_x(&self) -> f64;
76
77 fn max_x(&self) -> f64;
79
80 fn in_range(&self, x: f64) -> bool {
82 x >= self.min_x() && x <= self.max_x()
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use approx::assert_relative_eq;
90
91 #[test]
92 fn test_linear_basic() {
93 let xs = vec![0.0, 1.0, 2.0];
94 let ys = vec![0.0, 1.0, 2.0];
95
96 let interp = LinearInterpolator::new(xs, ys).unwrap();
97
98 assert_relative_eq!(interp.interpolate(0.5).unwrap(), 0.5, epsilon = 1e-10);
99 assert_relative_eq!(interp.interpolate(1.5).unwrap(), 1.5, epsilon = 1e-10);
100 }
101
102 #[test]
105 fn test_all_interpolators_through_points() {
106 let times = vec![0.5, 1.0, 2.0, 3.0, 5.0];
108 let rates = vec![0.02, 0.025, 0.03, 0.035, 0.04];
109
110 let linear = LinearInterpolator::new(times.clone(), rates.clone()).unwrap();
112 for (t, r) in times.iter().zip(rates.iter()) {
113 assert_relative_eq!(linear.interpolate(*t).unwrap(), *r, epsilon = 1e-10);
114 }
115
116 let spline = CubicSpline::new(times.clone(), rates.clone()).unwrap();
118 for (t, r) in times.iter().zip(rates.iter()) {
119 assert_relative_eq!(spline.interpolate(*t).unwrap(), *r, epsilon = 1e-10);
120 }
121
122 let dfs: Vec<f64> = times
124 .iter()
125 .zip(rates.iter())
126 .map(|(t, r)| (-r * t).exp())
127 .collect();
128 let log_linear = LogLinearInterpolator::new(times.clone(), dfs.clone()).unwrap();
129 for (t, df) in times.iter().zip(dfs.iter()) {
130 assert_relative_eq!(log_linear.interpolate(*t).unwrap(), *df, epsilon = 1e-10);
131 }
132
133 let mc = MonotoneConvex::new(times.clone(), rates.clone()).unwrap();
135 for (t, r) in times.iter().zip(rates.iter()) {
136 assert_relative_eq!(mc.interpolate(*t).unwrap(), *r, epsilon = 0.002);
138 }
139 }
140
141 #[test]
142 fn test_forward_rate_positivity() {
143 let times = vec![0.5, 1.0, 2.0, 3.0, 5.0, 10.0];
145 let rates = vec![0.02, 0.025, 0.03, 0.028, 0.035, 0.04];
146
147 let mc = MonotoneConvex::new(times, rates).unwrap();
148
149 for t in [
151 0.1, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.0, 10.0,
152 ] {
153 let fwd = mc.forward_rate(t).unwrap();
154 assert!(fwd >= 0.0, "Forward at t={} is {}, should be >= 0", t, fwd);
155 }
156 }
157
158 #[test]
159 fn test_derivative_consistency() {
160 let times = vec![0.5, 1.0, 2.0, 3.0, 5.0];
162 let rates = vec![0.02, 0.025, 0.03, 0.035, 0.04];
163
164 let linear = LinearInterpolator::new(times.clone(), rates.clone()).unwrap();
166 check_derivative(&linear, 1.5, "Linear");
167
168 let spline = CubicSpline::new(times.clone(), rates.clone()).unwrap();
170 check_derivative(&spline, 1.5, "CubicSpline");
171
172 let dfs: Vec<f64> = times
174 .iter()
175 .zip(rates.iter())
176 .map(|(t, r)| (-r * t).exp())
177 .collect();
178 let log_linear = LogLinearInterpolator::new(times.clone(), dfs).unwrap();
179 check_derivative(&log_linear, 1.5, "LogLinear");
180
181 let mc = MonotoneConvex::new(times.clone(), rates.clone()).unwrap();
183 check_derivative(&mc, 1.5, "MonotoneConvex");
184
185 let ns = NelsonSiegel::new(0.04, -0.02, 0.01, 2.0).unwrap();
187 check_derivative(&ns, 3.0, "NelsonSiegel");
188
189 let sv = Svensson::new(0.04, -0.02, 0.01, -0.005, 2.0, 8.0).unwrap();
191 check_derivative(&sv, 3.0, "Svensson");
192 }
193
194 fn check_derivative(interp: &dyn Interpolator, t: f64, name: &str) {
195 let h = 1e-6;
196 let y_plus = interp.interpolate(t + h).unwrap();
197 let y_minus = interp.interpolate(t - h).unwrap();
198 let numerical = (y_plus - y_minus) / (2.0 * h);
199
200 let analytical = interp.derivative(t).unwrap();
201
202 assert!(
203 (analytical - numerical).abs() < 1e-4,
204 "{} derivative at t={}: analytical={}, numerical={}",
205 name,
206 t,
207 analytical,
208 numerical
209 );
210 }
211
212 #[test]
213 fn test_yield_curve_construction() {
214 let maturities = vec![0.25, 0.5, 1.0, 2.0, 3.0, 5.0, 7.0, 10.0, 20.0, 30.0];
216 let zero_rates = vec![
217 0.0200, 0.0210, 0.0225, 0.0250, 0.0275, 0.0310, 0.0340, 0.0370, 0.0400, 0.0410,
218 ];
219
220 let linear = LinearInterpolator::new(maturities.clone(), zero_rates.clone()).unwrap();
222 let spline = CubicSpline::new(maturities.clone(), zero_rates.clone()).unwrap();
223 let mc = MonotoneConvex::new(maturities.clone(), zero_rates.clone()).unwrap();
224
225 let t = 4.0;
227 let z_linear = linear.interpolate(t).unwrap();
228 let z_spline = spline.interpolate(t).unwrap();
229 let z_mc = mc.interpolate(t).unwrap();
230
231 assert!(z_linear > 0.0275 && z_linear < 0.0310);
233 assert!(z_spline > 0.0275 && z_spline < 0.0310);
234 assert!(z_mc > 0.0275 && z_mc < 0.0310);
235
236 let fwd = mc.forward_rate(t).unwrap();
238 assert!(fwd > 0.0);
239 }
240
241 #[test]
242 fn test_parametric_curve_fitting() {
243 let ns = NelsonSiegel::new(0.04, -0.015, 0.008, 2.5).unwrap();
245
246 let r_1y = ns.interpolate(1.0).unwrap();
248 let r_10y = ns.interpolate(10.0).unwrap();
249 assert!(r_1y < r_10y);
250
251 let r_3y = ns.interpolate(3.0).unwrap();
253 let d_1y = ns.derivative(1.0).unwrap();
254 let d_10y = ns.derivative(10.0).unwrap();
255
256 assert!(r_3y > r_1y && r_3y < r_10y);
258
259 assert!(d_1y > 0.0);
261 assert!(d_1y > d_10y);
262 }
263
264 #[test]
265 fn test_discount_factor_interpolation() {
266 let times = vec![0.25, 0.5, 1.0, 2.0, 5.0, 10.0];
268 let rates = [0.02, 0.022, 0.025, 0.03, 0.035, 0.04];
269
270 let dfs: Vec<f64> = times
272 .iter()
273 .zip(rates.iter())
274 .map(|(t, r): (&f64, &f64)| (-r * t).exp())
275 .collect();
276
277 let log_linear = LogLinearInterpolator::new(times.clone(), dfs.clone()).unwrap();
278
279 let mut prev_df = 1.0;
281 for t in [0.25, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 7.0, 10.0] {
282 let df = log_linear.interpolate(t).unwrap();
283 assert!(
284 df < prev_df,
285 "DF at t={} ({}) should be < previous ({})",
286 t,
287 df,
288 prev_df
289 );
290 assert!(df > 0.0, "DF should be positive");
291 prev_df = df;
292 }
293 }
294}