convex_math/interpolation/
mod.rs

1//! Interpolation methods for yield curve construction.
2//!
3//! This module provides various interpolation algorithms commonly used
4//! in yield curve construction and financial calculations.
5//!
6//! # Available Methods
7//!
8//! **On Zero Rates:**
9//! - [`LinearInterpolator`]: Simple linear interpolation
10//! - [`LogLinearInterpolator`]: Log-linear interpolation (interpolates log of values)
11//! - [`CubicSpline`]: Natural cubic spline interpolation
12//! - [`MonotoneConvex`]: Hagan monotone convex (production default, ensures positive forwards)
13//! - [`FlatForward`]: Flat forward interpolation (constant forward rates between pillars)
14//!
15//! **Parametric Models:**
16//! - [`NelsonSiegel`]: Nelson-Siegel parametric curve
17//! - [`Svensson`]: Svensson extension of Nelson-Siegel
18//!
19//! # Choosing an Interpolation Method
20//!
21//! | Method | Speed | Smoothness | Positive Forwards | Use Case |
22//! |--------|-------|------------|-------------------|----------|
23//! | Linear | Fast | C0 | No | Quick prototyping |
24//! | Log-Linear | Fast | C0 | Yes (on discount) | Discount factor curves |
25//! | Flat Forward | Fast | C0 (step fwd) | Yes* | Step forward curve |
26//! | Cubic Spline | Medium | C2 | No | Smooth curves |
27//! | Monotone Convex | Medium | C1 | **Yes** | **Production default** |
28//! | Nelson-Siegel | Fast | C∞ | Usually | Parametric fitting |
29//! | Svensson | Fast | C∞ | Usually | More flexible fitting |
30//!
31//! *Flat forward preserves positive forwards if input zero rates produce positive forwards.
32//!
33//! # Forward Rate Considerations
34//!
35//! For production yield curve construction, use [`MonotoneConvex`] as it guarantees:
36//! - Positive forward rates
37//! - No spurious oscillations
38//! - C1 continuity (continuous first derivative)
39
40mod 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
56/// Trait for interpolation methods.
57///
58/// All interpolation methods implement this trait, providing a unified
59/// interface for curve construction.
60pub trait Interpolator: Send + Sync {
61    /// Returns the interpolated value at x.
62    fn interpolate(&self, x: f64) -> MathResult<f64>;
63
64    /// Returns the first derivative at x.
65    ///
66    /// This is critical for computing forward rates from zero rates.
67    fn derivative(&self, x: f64) -> MathResult<f64>;
68
69    /// Returns true if extrapolation is allowed.
70    fn allows_extrapolation(&self) -> bool {
71        false
72    }
73
74    /// Returns the minimum x value in the data.
75    fn min_x(&self) -> f64;
76
77    /// Returns the maximum x value in the data.
78    fn max_x(&self) -> f64;
79
80    /// Checks if x is within the interpolation range.
81    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    // ============ Comparative Tests ============
103
104    #[test]
105    fn test_all_interpolators_through_points() {
106        // All interpolators should pass through the input points
107        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        // Linear
111        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        // Cubic Spline
117        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        // Log-Linear (on discount factors)
123        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        // Monotone Convex
134        let mc = MonotoneConvex::new(times.clone(), rates.clone()).unwrap();
135        for (t, r) in times.iter().zip(rates.iter()) {
136            // Monotone convex may have small deviations due to smoothing
137            assert_relative_eq!(mc.interpolate(*t).unwrap(), *r, epsilon = 0.002);
138        }
139    }
140
141    #[test]
142    fn test_forward_rate_positivity() {
143        // Test that MonotoneConvex always produces positive forwards
144        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        // Check many points for positive forwards
150        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        // Test that derivatives are numerically correct for all interpolators
161        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        // Linear
165        let linear = LinearInterpolator::new(times.clone(), rates.clone()).unwrap();
166        check_derivative(&linear, 1.5, "Linear");
167
168        // Cubic Spline
169        let spline = CubicSpline::new(times.clone(), rates.clone()).unwrap();
170        check_derivative(&spline, 1.5, "CubicSpline");
171
172        // Log-Linear
173        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        // Monotone Convex
182        let mc = MonotoneConvex::new(times.clone(), rates.clone()).unwrap();
183        check_derivative(&mc, 1.5, "MonotoneConvex");
184
185        // Nelson-Siegel
186        let ns = NelsonSiegel::new(0.04, -0.02, 0.01, 2.0).unwrap();
187        check_derivative(&ns, 3.0, "NelsonSiegel");
188
189        // Svensson
190        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        // Realistic yield curve data
215        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        // Build curves with different methods
221        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        // Test interpolation at 4 years (between 3Y and 5Y pillars)
226        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        // All should give reasonable values between 2.75% and 3.10%
232        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        // MonotoneConvex should have positive forwards
237        let fwd = mc.forward_rate(t).unwrap();
238        assert!(fwd > 0.0);
239    }
240
241    #[test]
242    fn test_parametric_curve_fitting() {
243        // Test that parametric curves can model realistic shapes
244        let ns = NelsonSiegel::new(0.04, -0.015, 0.008, 2.5).unwrap();
245
246        // Should be upward sloping
247        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        // Should have a slight hump
252        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        // Mid-term rate should be between short and long
257        assert!(r_3y > r_1y && r_3y < r_10y);
258
259        // Derivative should be positive (upward slope) but decreasing
260        assert!(d_1y > 0.0);
261        assert!(d_1y > d_10y);
262    }
263
264    #[test]
265    fn test_discount_factor_interpolation() {
266        // Test log-linear for discount factors
267        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        // Convert to discount factors
271        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        // Interpolated discount factors should be monotonically decreasing
280        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}