convex_math/interpolation/
flat_forward.rs1use crate::error::{MathError, MathResult};
27use crate::interpolation::Interpolator;
28
29#[derive(Debug, Clone)]
49pub struct FlatForward {
50 tenors: Vec<f64>,
52 zero_rates: Vec<f64>,
54 forward_rates: Vec<f64>,
56 allow_extrapolation: bool,
58}
59
60impl FlatForward {
61 pub fn new(tenors: Vec<f64>, zero_rates: Vec<f64>) -> MathResult<Self> {
76 if tenors.len() < 2 {
77 return Err(MathError::insufficient_data(2, tenors.len()));
78 }
79 if tenors.len() != zero_rates.len() {
80 return Err(MathError::invalid_input(format!(
81 "tenors and zero_rates must have same length: {} vs {}",
82 tenors.len(),
83 zero_rates.len()
84 )));
85 }
86
87 if tenors[0] <= 0.0 {
89 return Err(MathError::invalid_input(
90 "First tenor must be positive for flat forward interpolation",
91 ));
92 }
93 for i in 1..tenors.len() {
94 if tenors[i] <= tenors[i - 1] {
95 return Err(MathError::invalid_input(
96 "Tenors must be strictly increasing",
97 ));
98 }
99 }
100
101 let forward_rates = Self::compute_forward_rates(&tenors, &zero_rates);
103
104 Ok(Self {
105 tenors,
106 zero_rates,
107 forward_rates,
108 allow_extrapolation: false,
109 })
110 }
111
112 pub fn with_origin(mut tenors: Vec<f64>, mut zero_rates: Vec<f64>) -> MathResult<Self> {
122 if tenors.is_empty() {
123 return Err(MathError::insufficient_data(1, 0));
124 }
125
126 if tenors[0] > 0.0 {
128 tenors.insert(0, 0.0);
130 zero_rates.insert(0, zero_rates[0]);
131 }
132
133 if tenors.len() < 2 {
136 return Err(MathError::insufficient_data(2, tenors.len()));
137 }
138 if tenors.len() != zero_rates.len() {
139 return Err(MathError::invalid_input(format!(
140 "tenors and zero_rates must have same length: {} vs {}",
141 tenors.len(),
142 zero_rates.len()
143 )));
144 }
145
146 for i in 1..tenors.len() {
147 if tenors[i] <= tenors[i - 1] {
148 return Err(MathError::invalid_input(
149 "Tenors must be strictly increasing",
150 ));
151 }
152 }
153
154 let forward_rates = Self::compute_forward_rates(&tenors, &zero_rates);
155
156 Ok(Self {
157 tenors,
158 zero_rates,
159 forward_rates,
160 allow_extrapolation: false,
161 })
162 }
163
164 #[must_use]
170 pub fn with_extrapolation(mut self) -> Self {
171 self.allow_extrapolation = true;
172 self
173 }
174
175 fn compute_forward_rates(tenors: &[f64], zero_rates: &[f64]) -> Vec<f64> {
180 let n = tenors.len();
181 let mut forwards = Vec::with_capacity(n);
182
183 for i in 0..n - 1 {
184 let t0 = tenors[i];
185 let t1 = tenors[i + 1];
186 let r0 = zero_rates[i];
187 let r1 = zero_rates[i + 1];
188
189 let fwd = if t0 == 0.0 {
191 r1
193 } else {
194 (r1 * t1 - r0 * t0) / (t1 - t0)
195 };
196
197 forwards.push(fwd);
198 }
199
200 if !forwards.is_empty() {
202 forwards.push(*forwards.last().unwrap());
203 } else {
204 forwards.push(zero_rates[0]);
205 }
206
207 forwards
208 }
209
210 fn find_segment(&self, t: f64) -> usize {
214 match self
215 .tenors
216 .binary_search_by(|probe| probe.partial_cmp(&t).unwrap_or(std::cmp::Ordering::Equal))
217 {
218 Ok(i) => i.min(self.tenors.len() - 2),
219 Err(i) => (i.saturating_sub(1)).min(self.tenors.len() - 2),
220 }
221 }
222
223 pub fn forward_rate(&self, t: f64) -> MathResult<f64> {
228 if !self.allow_extrapolation && (t < self.tenors[0] || t > *self.tenors.last().unwrap()) {
229 return Err(MathError::ExtrapolationNotAllowed {
230 x: t,
231 min: self.tenors[0],
232 max: *self.tenors.last().unwrap(),
233 });
234 }
235
236 let i = self.find_segment(t);
237 Ok(self.forward_rates[i])
238 }
239
240 pub fn tenors(&self) -> &[f64] {
242 &self.tenors
243 }
244
245 pub fn zero_rates(&self) -> &[f64] {
247 &self.zero_rates
248 }
249
250 pub fn forward_rates_vec(&self) -> &[f64] {
252 &self.forward_rates
253 }
254}
255
256impl Interpolator for FlatForward {
257 fn interpolate(&self, t: f64) -> MathResult<f64> {
258 let min_t = self.tenors[0];
259 let max_t = *self.tenors.last().unwrap();
260
261 if !self.allow_extrapolation && (t < min_t || t > max_t) {
263 return Err(MathError::ExtrapolationNotAllowed {
264 x: t,
265 min: min_t,
266 max: max_t,
267 });
268 }
269
270 if t <= 0.0 {
272 return Ok(self.zero_rates[0]);
274 }
275
276 if let Some(idx) = self.tenors.iter().position(|&x| (x - t).abs() < 1e-12) {
278 return Ok(self.zero_rates[idx]);
279 }
280
281 if t < min_t {
283 return Ok(self.zero_rates[0]);
287 }
288
289 if t > max_t {
291 let n = self.tenors.len();
293 let t_n = self.tenors[n - 1];
294 let r_n = self.zero_rates[n - 1];
295 let f_n = self.forward_rates[n - 1];
296
297 return Ok((r_n * t_n + f_n * (t - t_n)) / t);
299 }
300
301 let i = self.find_segment(t);
303 let t_i = self.tenors[i];
304 let r_i = self.zero_rates[i];
305 let f_i = self.forward_rates[i];
306
307 Ok((r_i * t_i + f_i * (t - t_i)) / t)
309 }
310
311 fn derivative(&self, t: f64) -> MathResult<f64> {
312 let min_t = self.tenors[0];
313 let max_t = *self.tenors.last().unwrap();
314
315 if !self.allow_extrapolation && (t < min_t || t > max_t) {
317 return Err(MathError::ExtrapolationNotAllowed {
318 x: t,
319 min: min_t,
320 max: max_t,
321 });
322 }
323
324 if t <= 0.0 {
325 return Ok(0.0); }
327
328 let i = if t > max_t {
330 self.tenors.len() - 2
331 } else if t < min_t {
332 0
333 } else {
334 self.find_segment(t)
335 };
336
337 let t_i = self.tenors[i];
338 let r_i = self.zero_rates[i];
339 let f_i = self.forward_rates[i];
340
341 Ok((f_i - r_i) * t_i / (t * t))
351 }
352
353 fn allows_extrapolation(&self) -> bool {
354 self.allow_extrapolation
355 }
356
357 fn min_x(&self) -> f64 {
358 self.tenors[0]
359 }
360
361 fn max_x(&self) -> f64 {
362 *self.tenors.last().unwrap()
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use approx::assert_relative_eq;
370
371 #[test]
372 fn test_flat_forward_through_pillars() {
373 let tenors = vec![1.0, 2.0, 5.0, 10.0];
374 let zero_rates = vec![0.02, 0.025, 0.03, 0.035];
375
376 let interp = FlatForward::new(tenors.clone(), zero_rates.clone()).unwrap();
377
378 for (t, r) in tenors.iter().zip(zero_rates.iter()) {
380 assert_relative_eq!(interp.interpolate(*t).unwrap(), *r, epsilon = 1e-10);
381 }
382 }
383
384 #[test]
385 fn test_flat_forward_rates() {
386 let tenors = vec![1.0, 2.0, 3.0];
387 let zero_rates = vec![0.02, 0.03, 0.04];
388
389 let interp = FlatForward::new(tenors, zero_rates).unwrap();
390
391 assert_relative_eq!(interp.forward_rate(1.5).unwrap(), 0.04, epsilon = 1e-10);
394
395 assert_relative_eq!(interp.forward_rate(2.5).unwrap(), 0.06, epsilon = 1e-10);
398 }
399
400 #[test]
401 fn test_interpolation_between_pillars() {
402 let tenors = vec![1.0, 2.0];
403 let zero_rates = vec![0.02, 0.04];
404
405 let interp = FlatForward::new(tenors, zero_rates).unwrap();
406
407 let r_mid = interp.interpolate(1.5).unwrap();
411 assert_relative_eq!(r_mid, 0.05 / 1.5, epsilon = 1e-10);
412 }
413
414 #[test]
415 fn test_forward_rate_consistency() {
416 let tenors = vec![1.0, 2.0, 5.0, 10.0];
418 let zero_rates = vec![0.02, 0.025, 0.03, 0.035];
419
420 let interp = FlatForward::new(tenors.clone(), zero_rates.clone()).unwrap();
421
422 let f_segment_1 = interp.forward_rate(1.5).unwrap();
424 assert_relative_eq!(
425 interp.forward_rate(1.1).unwrap(),
426 f_segment_1,
427 epsilon = 1e-10
428 );
429 assert_relative_eq!(
430 interp.forward_rate(1.9).unwrap(),
431 f_segment_1,
432 epsilon = 1e-10
433 );
434
435 let expected_f = (0.025 * 2.0 - 0.02 * 1.0) / (2.0 - 1.0);
438 assert_relative_eq!(f_segment_1, expected_f, epsilon = 1e-10);
439 }
440
441 #[test]
442 fn test_positive_forward_rates() {
443 let tenors = vec![1.0, 2.0, 5.0, 10.0];
445 let zero_rates = vec![0.02, 0.025, 0.03, 0.035];
446
447 let interp = FlatForward::new(tenors, zero_rates).unwrap();
448
449 for &f in interp.forward_rates_vec() {
451 assert!(f > 0.0, "Forward rate {} should be positive", f);
452 }
453 }
454
455 #[test]
456 fn test_derivative_numerical() {
457 let tenors = vec![1.0, 2.0, 5.0, 10.0];
458 let zero_rates = vec![0.02, 0.025, 0.03, 0.035];
459
460 let interp = FlatForward::new(tenors, zero_rates)
461 .unwrap()
462 .with_extrapolation();
463
464 for t in [1.5, 2.5, 4.0, 7.0] {
466 let h = 1e-6;
467 let r_plus = interp.interpolate(t + h).unwrap();
468 let r_minus = interp.interpolate(t - h).unwrap();
469 let numerical = (r_plus - r_minus) / (2.0 * h);
470 let analytical = interp.derivative(t).unwrap();
471
472 assert_relative_eq!(analytical, numerical, epsilon = 1e-5);
473 }
474 }
475
476 #[test]
477 fn test_extrapolation() {
478 let tenors = vec![1.0, 2.0, 5.0];
479 let zero_rates = vec![0.02, 0.025, 0.03];
480
481 let interp = FlatForward::new(tenors, zero_rates)
482 .unwrap()
483 .with_extrapolation();
484
485 assert!(interp.interpolate(0.5).is_ok());
487 assert!(interp.interpolate(7.0).is_ok());
488
489 assert_relative_eq!(interp.interpolate(0.5).unwrap(), 0.02, epsilon = 1e-10);
491 }
492
493 #[test]
494 fn test_no_extrapolation() {
495 let tenors = vec![1.0, 2.0, 5.0];
496 let zero_rates = vec![0.02, 0.025, 0.03];
497
498 let interp = FlatForward::new(tenors, zero_rates).unwrap();
499
500 assert!(interp.interpolate(0.5).is_err());
502 assert!(interp.interpolate(7.0).is_err());
503 }
504
505 #[test]
506 fn test_with_origin() {
507 let tenors = vec![1.0, 2.0, 5.0];
508 let zero_rates = vec![0.02, 0.025, 0.03];
509
510 let interp = FlatForward::with_origin(tenors, zero_rates).unwrap();
511
512 assert!(interp.interpolate(0.0).is_ok());
514 assert!(interp.interpolate(0.5).is_ok());
515 }
516
517 #[test]
518 fn test_insufficient_points() {
519 let tenors = vec![1.0];
520 let zero_rates = vec![0.02];
521
522 assert!(FlatForward::new(tenors, zero_rates).is_err());
523 }
524
525 #[test]
526 fn test_mismatched_lengths() {
527 let tenors = vec![1.0, 2.0, 3.0];
528 let zero_rates = vec![0.02, 0.025];
529
530 assert!(FlatForward::new(tenors, zero_rates).is_err());
531 }
532
533 #[test]
534 fn test_non_positive_tenor() {
535 let tenors = vec![0.0, 1.0, 2.0];
536 let zero_rates = vec![0.02, 0.025, 0.03];
537
538 assert!(FlatForward::new(tenors, zero_rates).is_err());
540 }
541
542 #[test]
543 fn test_flat_curve() {
544 let tenors = vec![1.0, 2.0, 5.0, 10.0];
546 let zero_rates = vec![0.03, 0.03, 0.03, 0.03];
547
548 let interp = FlatForward::new(tenors.clone(), zero_rates).unwrap();
549
550 for &f in interp.forward_rates_vec() {
552 assert_relative_eq!(f, 0.03, epsilon = 1e-10);
553 }
554
555 for t in [1.0, 1.5, 2.5, 4.0, 7.0, 10.0] {
557 assert_relative_eq!(interp.interpolate(t).unwrap(), 0.03, epsilon = 1e-10);
558 }
559 }
560
561 #[test]
562 fn test_inverted_curve() {
563 let tenors = vec![1.0, 2.0, 5.0, 10.0];
565 let zero_rates = vec![0.05, 0.04, 0.03, 0.025];
566
567 let interp = FlatForward::new(tenors, zero_rates).unwrap();
568
569 assert!(interp.interpolate(1.5).is_ok());
572 assert!(interp.interpolate(3.0).is_ok());
573 }
574}