Skip to main content

rustyqlib/rates/
term_structure.rs

1/// Term Structure Module
2///
3/// Provides types for managing interest rate term structures with various
4/// day count conventions and interpolation methods.
5use std::fmt;
6use crate::core::errors::RustyQLibError;
7
8// ─── Day Count Convention ────────────────────────────────────────────────────
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum DayCountConvention {
12    Actual360,
13    Actual365,
14    ActualActual,
15    Thirty360,
16    Thirty360European,
17}
18
19impl fmt::Display for DayCountConvention {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        let s = match self {
22            Self::Actual360 => "Actual/360",
23            Self::Actual365 => "Actual/365",
24            Self::ActualActual => "Actual/Actual",
25            Self::Thirty360 => "30/360",
26            Self::Thirty360European => "30E/360",
27        };
28        write!(f, "{s}")
29    }
30}
31
32fn is_leap_year(year: i32) -> bool {
33    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
34}
35
36fn days_in_year(year: i32) -> f64 {
37    if is_leap_year(year) { 366.0 } else { 365.0 }
38}
39
40/// A simple date type (calendar date only, no time component).
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
42pub struct Date {
43    pub year: i32,
44    pub month: u32,
45    pub day: u32,
46}
47
48impl Date {
49    pub fn new(year: i32, month: u32, day: u32) -> Self {
50        Self { year, month, day }
51    }
52
53    /// Days since the Unix epoch (1970-01-01), proleptic Gregorian – used
54    /// for day differences and as the inverse of [`from_epoch`]. The
55    /// `- 719468` offset shifts the internal 0000-03-01 era count onto the
56    /// Unix epoch so that `from_epoch(days_since_epoch(d)) == d`.
57    fn days_since_epoch(self) -> i64 {
58        let y = self.year as i64;
59        let m = self.month as i64;
60        let d = self.day as i64;
61        // Shift months so March = 1, to simplify leap-year handling
62        let (y, m) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) };
63        let era = y.div_euclid(400);
64        let yoe = y.rem_euclid(400); // year of era [0, 399]
65        let doy = (153 * m + 2) / 5 + d - 1; // day of year [0, 365]
66        let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // day of era
67        era * 146097 + doe - 719468
68    }
69
70    /// Compute the number of calendar days between two dates (self is start).
71    pub fn days_until(self, other: Date) -> i64 {
72        other.days_since_epoch() - self.days_since_epoch()
73    }
74
75    /// Add a given number of days to this date.
76    pub fn add_days(self, days: i64) -> Date {
77        // Leverage epoch arithmetic
78        let epoch = self.days_since_epoch() + days;
79        Date::from_epoch(epoch)
80    }
81
82    fn from_epoch(z: i64) -> Date {
83        let z = z + 719468; // shift to civil epoch
84        let era = z.div_euclid(146097);
85        let doe = z.rem_euclid(146097);
86        let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
87        let y = yoe + era * 400;
88        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
89        let mp = (5 * doy + 2) / 153;
90        let d = doy - (153 * mp + 2) / 5 + 1;
91        let (y, m) = if mp < 10 { (y, mp + 3) } else { (y + 1, mp - 9) };
92        Date::new(y as i32, m as u32, d as u32)
93    }
94
95    /// Last day of the month for this date.
96    pub fn days_in_month(self) -> u32 {
97        match self.month {
98            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
99            4 | 6 | 9 | 11 => 30,
100            2 => if is_leap_year(self.year) { 29 } else { 28 },
101            _ => panic!("invalid month {}", self.month),
102        }
103    }
104}
105
106impl fmt::Display for Date {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
109    }
110}
111
112impl DayCountConvention {
113    fn actual_actual(self, start: Date, end: Date) -> f64 {
114        let days = start.days_until(end);
115        let y1 = start.year;
116        let y2 = end.year;
117
118        if y1 == y2 {
119            return days as f64 / days_in_year(y1);
120        }
121
122        // Multi-year: accumulate fractional years
123        let mut total = 0.0_f64;
124        let mut current = start;
125
126        while current.year < y2 {
127            let year_end = Date::new(current.year, 12, 31);
128            let days_to_end = current.days_until(year_end) + 1;
129            total += days_to_end as f64 / days_in_year(current.year);
130            current = Date::new(current.year + 1, 1, 1);
131        }
132
133        let days_in_final = current.days_until(end);
134        total += days_in_final as f64 / days_in_year(y2);
135        total
136    }
137
138    /// Compute the year fraction between two dates under this convention.
139    pub fn year_fraction(self, start: Date, end: Date) -> f64 {
140        let days = start.days_until(end) as f64;
141
142        match self {
143            Self::Actual360 => days / 360.0,
144            Self::Actual365 => days / 365.0,
145            Self::ActualActual => self.actual_actual(start, end),
146
147            Self::Thirty360 => {
148                let (mut d1, m1, y1) = (start.day as i32, start.month as i32, start.year);
149                let (mut d2, m2, y2) = (end.day as i32, end.month as i32, end.year);
150                // 30/360 US (Bond Basis)
151                if d1 == 31 { d1 = 30; }
152                if d2 == 31 && d1 >= 30 { d2 = 30; }
153                // TODO: Add Feb end-of-month adjustment for 30/360 US
154                let days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1);
155                days as f64 / 360.0
156            }
157
158            Self::Thirty360European => {
159                let (mut d1, m1, y1) = (start.day as i32, start.month as i32, start.year);
160                let (mut d2, m2, y2) = (end.day as i32, end.month as i32, end.year);
161                if d1 == 31 { d1 = 30; }
162                if d2 == 31 { d2 = 30; }
163                let days = 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1);
164                days as f64 / 360.0
165            }
166        }
167    }
168}
169
170// ─── Interpolation Method ────────────────────────────────────────────────────
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173pub enum InterpolationMethod {
174    Linear,
175    LogLinear,
176    CubicSpline,
177    FlatForward,
178}
179
180impl fmt::Display for InterpolationMethod {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        let s = match self {
183            Self::Linear => "linear",
184            Self::LogLinear => "log_linear",
185            Self::CubicSpline => "cubic_spline",
186            Self::FlatForward => "flat_forward",
187        };
188        write!(f, "{s}")
189    }
190}
191
192// ─── Term Structure ──────────────────────────────────────────────────────────
193
194/// An interest rate term structure.
195///
196/// Stores a grid of (date, discount-factor) pairs and derives zero rates and
197/// an interpolator for off-grid queries.
198#[derive(Debug, Clone)]
199pub struct TermStructure {
200    pub dates: Vec<Date>,
201    pub discount_factors: Vec<f64>,
202    pub day_count_convention: DayCountConvention,
203    pub interpolation_method: InterpolationMethod,
204    pub asof_date: Date,
205
206    // Derived / cached fields
207    pub year_fractions: Vec<f64>,
208    pub zero_rates: Vec<f64>,
209
210    // For cubic-spline: precomputed second derivatives
211    spline_m: Vec<f64>,
212}
213
214impl TermStructure {
215    // ── Constructors ─────────────────────────────────────────────────────────
216
217    /// Create a new term structure from dates and discount factors.
218    pub fn new(
219        dates: Vec<Date>,
220        discount_factors: Vec<f64>,
221        day_count_convention: DayCountConvention,
222        interpolation_method: InterpolationMethod,
223        asof_date: Date,
224    ) -> Result<Self, RustyQLibError> {
225        // Validation
226        if dates.len() != discount_factors.len() {
227            return Err(RustyQLibError::invalid_input("term structure", "dates and discount_factors must have the same length"));
228        }
229        if dates.len() < 2 {
230            return Err(RustyQLibError::invalid_input("term structure", "Need at least 2 points to define a term structure"));
231        }
232        if !dates.windows(2).all(|w| w[0] < w[1]) {
233            return Err(RustyQLibError::invalid_input("term structure", "dates must be strictly increasing"));
234        }
235        if !discount_factors.iter().all(|&df| df > 0.0) {
236            return Err(RustyQLibError::invalid_input("term structure", "Discount factors must be positive"));
237        }
238        if !discount_factors.iter().all(|&df| df <= 1.0) {
239            return Err(RustyQLibError::invalid_input("term structure", "Discount factors must be <= 1.0"));
240        }
241
242        let year_fractions: Vec<f64> = dates
243            .iter()
244            .map(|&d| day_count_convention.year_fraction(asof_date, d))
245            .collect();
246
247        let zero_rates: Vec<f64> = discount_factors
248            .iter()
249            .zip(year_fractions.iter())
250            .map(|(&df, &t)| {
251                if t > 0.0 && df > 0.0 { -df.ln() / t } else { 0.0 }
252            })
253            .collect();
254
255        let spline_m = if interpolation_method == InterpolationMethod::CubicSpline {
256            compute_natural_spline(&year_fractions, &discount_factors)
257        } else {
258            vec![]
259        };
260
261        Ok(Self {
262            dates,
263            discount_factors,
264            day_count_convention,
265            interpolation_method,
266            asof_date,
267            year_fractions,
268            zero_rates,
269            spline_m,
270        })
271    }
272
273    /// Build a flat (constant-rate) term structure.
274    pub fn flat_curve(
275        rate: f64,
276        asof_date: Date,
277        day_count_convention: DayCountConvention,
278        max_tenor_years: f64,
279    ) -> Result<Self, RustyQLibError> {
280        let tenors = [0.001, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0, 15.0, max_tenor_years];
281        let dates: Vec<Date> = tenors
282            .iter()
283            .map(|&t| asof_date.add_days((t * 365.25) as i64))
284            .collect();
285        let discount_factors: Vec<f64> = tenors.iter().map(|&t| (-rate * t).exp()).collect();
286
287        Self::new(
288            dates,
289            discount_factors,
290            day_count_convention,
291            InterpolationMethod::LogLinear,
292            asof_date,
293        )
294    }
295
296    /// Build a term structure from continuously-compounded zero rates.
297    pub fn from_zero_rates(
298        dates: Vec<Date>,
299        zero_rates: Vec<f64>,
300        day_count_convention: DayCountConvention,
301        asof_date: Date,
302    ) -> Result<Self, RustyQLibError> {
303        if dates.len() != zero_rates.len() {
304            return Err(RustyQLibError::invalid_input("term structure", "dates and zero_rates must have the same length"));
305        }
306        let discount_factors: Vec<f64> = dates
307            .iter()
308            .zip(zero_rates.iter())
309            .map(|(&d, &r)| {
310                let t = day_count_convention.year_fraction(asof_date, d);
311                (-r * t).exp()
312            })
313            .collect();
314
315        Self::new(dates, discount_factors, day_count_convention, InterpolationMethod::LogLinear, asof_date)
316    }
317
318    // ── Core query methods ────────────────────────────────────────────────────
319
320    /// Discount factor for a given maturity date.
321    pub fn discount_factor(&self, maturity_date: Date) -> f64 {
322        let t = self.day_count_convention.year_fraction(self.asof_date, maturity_date);
323        self.discount_factor_at_time(t)
324    }
325
326    /// Discount factor at time `t` (in years).
327    pub fn discount_factor_at_time(&self, t: f64) -> f64 {
328        if t <= 0.0 {
329            return 1.0;
330        }
331        match self.interpolation_method {
332            InterpolationMethod::Linear => {
333                interp_linear(&self.year_fractions, &self.discount_factors, t)
334            }
335            InterpolationMethod::LogLinear => {
336                let log_dfs: Vec<f64> = self.discount_factors.iter().map(|df| df.ln()).collect();
337                interp_linear(&self.year_fractions, &log_dfs, t).exp()
338            }
339            InterpolationMethod::CubicSpline => {
340                interp_cubic(&self.year_fractions, &self.discount_factors, &self.spline_m, t)
341            }
342            InterpolationMethod::FlatForward => {
343                flat_forward_df(&self.year_fractions, &self.discount_factors, t)
344            }
345        }
346    }
347
348    /// Zero-coupon rate (continuously compounded) for a given maturity date.
349    pub fn zero_rate(&self, maturity_date: Date) -> f64 {
350        let t = self.day_count_convention.year_fraction(self.asof_date, maturity_date);
351        self.zero_rate_at_time(t)
352    }
353
354    /// Zero-coupon rate at time `t` (in years).
355    pub fn zero_rate_at_time(&self, t: f64) -> f64 {
356        if t <= 0.0 {
357            return 0.0;
358        }
359        let df = self.discount_factor_at_time(t);
360        if df > 0.0 { -df.ln() / t } else { 0.0 }
361    }
362
363    /// Continuously-compounded forward rate between two dates.
364    pub fn forward_rate(&self, start_date: Date, end_date: Date) -> Result<f64, RustyQLibError> {
365        let t1 = self.day_count_convention.year_fraction(self.asof_date, start_date);
366        let t2 = self.day_count_convention.year_fraction(self.asof_date, end_date);
367        self.forward_rate_at_time(t1, t2)
368    }
369
370    /// Continuously-compounded forward rate between two times.
371    pub fn forward_rate_at_time(&self, t1: f64, t2: f64) -> Result<f64, RustyQLibError> {
372        if t2 <= t1 {
373            return Err(RustyQLibError::invalid_input("term structure", "t2 must be greater than t1"));
374        }
375        let df1 = self.discount_factor_at_time(t1);
376        let df2 = self.discount_factor_at_time(t2);
377        if df1 > 0.0 && df2 > 0.0 {
378            Ok(-(df2 / df1).ln() / (t2 - t1))
379        } else {
380            Ok(0.0)
381        }
382    }
383
384    // ── Display helpers ───────────────────────────────────────────────────────
385
386    pub fn summary(&self) -> String {
387        let mut lines = vec![
388            "Term Structure Summary".to_string(),
389            "=".repeat(70),
390            format!("Reference Date:       {}", self.asof_date),
391            format!("Day Count Convention: {}", self.day_count_convention),
392            format!("Interpolation Method: {}", self.interpolation_method),
393            format!("Number of Points:     {}", self.dates.len()),
394            String::new(),
395            format!(
396                "{:<12} {:<12} {:<18} {:<12}",
397                "Date", "Year Frac", "Discount Factor", "Zero Rate"
398            ),
399            "-".repeat(70),
400        ];
401
402        for (((date, &t), &df), &rate) in self
403            .dates.iter()
404            .zip(self.year_fractions.iter())
405            .zip(self.discount_factors.iter())
406            .zip(self.zero_rates.iter())
407        {
408            lines.push(format!(
409                "{:<12} {:<12.6} {:<18.6} {:.4}%",
410                date.to_string(),
411                t,
412                df,
413                rate * 100.0
414            ));
415        }
416
417        lines.join("\n")
418    }
419}
420
421impl fmt::Display for TermStructure {
422    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
423        write!(
424            f,
425            "TermStructure(asof_date={}, points={}, day_count={}, interpolation={})",
426            self.asof_date,
427            self.dates.len(),
428            self.day_count_convention,
429            self.interpolation_method,
430        )
431    }
432}
433
434// ─── Interpolation helpers ────────────────────────────────────────────────────
435
436/// Find the index `i` such that xs[i] <= x < xs[i+1].
437/// Clamps to valid range; extrapolates flat beyond endpoints.
438fn search_sorted(xs: &[f64], x: f64) -> usize {
439    match xs.binary_search_by(|probe| probe.partial_cmp(&x).unwrap()) {
440        Ok(i) => i.min(xs.len() - 2),
441        Err(i) => {
442            if i == 0 { 0 }
443            else if i >= xs.len() { xs.len() - 2 }
444            else { i - 1 }
445        }
446    }
447}
448
449/// Piecewise-linear interpolation (with linear extrapolation).
450fn interp_linear(xs: &[f64], ys: &[f64], x: f64) -> f64 {
451    let n = xs.len();
452    if x <= xs[0] {
453        // Linear extrapolation on the left
454        let slope = (ys[1] - ys[0]) / (xs[1] - xs[0]);
455        return ys[0] + slope * (x - xs[0]);
456    }
457    if x >= xs[n - 1] {
458        // Linear extrapolation on the right
459        let slope = (ys[n - 1] - ys[n - 2]) / (xs[n - 1] - xs[n - 2]);
460        return ys[n - 1] + slope * (x - xs[n - 1]);
461    }
462    let i = search_sorted(xs, x);
463    let t = (x - xs[i]) / (xs[i + 1] - xs[i]);
464    ys[i] * (1.0 - t) + ys[i + 1] * t
465}
466
467/// Compute second derivatives for a natural cubic spline (tridiagonal solve).
468fn compute_natural_spline(xs: &[f64], ys: &[f64]) -> Vec<f64> {
469    let n = xs.len();
470    let m = vec![0.0_f64; n]; // second derivatives (moments)
471    if n < 3 {
472        return m;
473    }
474
475    // Thomas algorithm for tridiagonal system
476    let h: Vec<f64> = (0..n - 1).map(|i| xs[i + 1] - xs[i]).collect();
477    let mut alpha: Vec<f64> = vec![0.0; n];
478    for i in 1..n - 1 {
479        alpha[i] = 3.0 / h[i] * (ys[i + 1] - ys[i]) - 3.0 / h[i - 1] * (ys[i] - ys[i - 1]);
480    }
481
482    let mut c = vec![0.0_f64; n];
483    let mut l = vec![1.0_f64; n];
484    let mut mu = vec![0.0_f64; n];
485    let mut z = vec![0.0_f64; n];
486
487    for i in 1..n - 1 {
488        l[i] = 2.0 * (xs[i + 1] - xs[i - 1]) - h[i - 1] * mu[i - 1];
489        mu[i] = h[i] / l[i];
490        z[i] = (alpha[i] - h[i - 1] * z[i - 1]) / l[i];
491    }
492
493    for j in (0..n - 1).rev() {
494        c[j] = z[j] - mu[j] * c[j + 1];
495    }
496    // c contains the second derivatives / 2; return 2*c = full second derivatives
497    c.iter().map(|&v| 2.0 * v).collect()
498}
499
500/// Evaluate a natural cubic spline at `x` given precomputed second derivatives `m`.
501fn interp_cubic(xs: &[f64], ys: &[f64], m: &[f64], x: f64) -> f64 {
502    let n = xs.len();
503    if x <= xs[0] { return ys[0]; }
504    if x >= xs[n - 1] { return ys[n - 1]; }
505
506    let i = search_sorted(xs, x);
507    let h = xs[i + 1] - xs[i];
508    let a = ys[i];
509    let b = (ys[i + 1] - ys[i]) / h - h * (2.0 * m[i] + m[i + 1]) / 6.0;
510    let c = m[i] / 2.0;
511    let d = (m[i + 1] - m[i]) / (6.0 * h);
512    let dt = x - xs[i];
513    a + b * dt + c * dt * dt + d * dt * dt * dt
514}
515
516/// Flat-forward discount-factor interpolation.
517fn flat_forward_df(xs: &[f64], dfs: &[f64], t: f64) -> f64 {
518    let n = xs.len();
519    if t <= xs[0] { return dfs[0]; }
520
521    for i in 0..n - 1 {
522        if xs[i] <= t && t <= xs[i + 1] {
523            let (t1, t2) = (xs[i], xs[i + 1]);
524            let (df1, df2) = (dfs[i], dfs[i + 1]);
525            if t2 > t1 {
526                let fwd_rate = -(df2 / df1).ln() / (t2 - t1);
527                return df1 * (-fwd_rate * (t - t1)).exp();
528            } else {
529                return df1;
530            }
531        }
532    }
533
534    // Beyond the last point: extrapolate with the last forward rate
535    if n >= 2 {
536        let (t1, t2) = (xs[n - 2], xs[n - 1]);
537        let (df1, df2) = (dfs[n - 2], dfs[n - 1]);
538        if t2 > t1 {
539            let fwd_rate = -(df2 / df1).ln() / (t2 - t1);
540            return dfs[n - 1] * (-fwd_rate * (t - t2)).exp();
541        }
542    }
543    dfs[n - 1]
544}
545
546// ─── Tests ───────────────────────────────────────────────────────────────────
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551
552    fn sample_ts() -> TermStructure {
553        let asof = Date::new(2024, 1, 1);
554        TermStructure::flat_curve(0.05, asof, DayCountConvention::Actual365, 30.0).unwrap()
555    }
556
557    #[test]
558    fn test_date_arithmetic() {
559        // 2024 is a leap year (366 days), so 365 days after Jan 1 is Dec 31
560        assert_eq!(Date::new(2024, 1, 1).add_days(365), Date::new(2024, 12, 31));
561        assert_eq!(Date::new(2024, 1, 1).add_days(366), Date::new(2025, 1, 1));
562        // 2023 is not a leap year
563        assert_eq!(Date::new(2023, 1, 1).add_days(365), Date::new(2024, 1, 1));
564        // round trip through the epoch, including a month/leap-day boundary
565        let d = Date::new(2024, 2, 28);
566        assert_eq!(d.add_days(1), Date::new(2024, 2, 29));
567        assert_eq!(d.add_days(2), Date::new(2024, 3, 1));
568        assert_eq!(d.days_until(d.add_days(400)), 400);
569    }
570
571    #[test]
572    fn test_flat_curve_discount_factor() {
573        let ts = sample_ts();
574        let d = ts.asof_date.add_days(365);
575        let df = ts.discount_factor(d);
576        // For a flat 5% curve over 1 year: df ≈ exp(-0.05)
577        let expected = (-0.05_f64).exp();
578        assert!((df - expected).abs() < 1e-3, "df={df}, expected={expected}");
579    }
580
581    #[test]
582    fn test_zero_rate_roundtrip() {
583        let ts = sample_ts();
584        let d = ts.asof_date.add_days(730); // 2 years
585        let r = ts.zero_rate(d);
586        assert!((r - 0.05).abs() < 1e-3, "zero rate={r}");
587    }
588
589    #[test]
590    fn test_forward_rate() {
591        let ts = sample_ts();
592        let t1 = ts.asof_date.add_days(365);
593        let t2 = ts.asof_date.add_days(730);
594        let fwd = ts.forward_rate(t1, t2).unwrap();
595        // On a flat curve the forward rate equals the spot rate
596        assert!((fwd - 0.05).abs() < 1e-3, "fwd={fwd}");
597    }
598
599    #[test]
600    fn test_from_zero_rates() {
601        let asof = Date::new(2024, 1, 1);
602        let dates = vec![
603            asof.add_days(365),
604            asof.add_days(730),
605            asof.add_days(1825),
606        ];
607        let rates = vec![0.04, 0.045, 0.05];
608        let ts = TermStructure::from_zero_rates(
609            dates, rates, DayCountConvention::Actual365, asof,
610        ).unwrap();
611        assert!((ts.zero_rates[0] - 0.04).abs() < 1e-6);
612    }
613
614    #[test]
615    fn test_thirty_360() {
616        let d1 = Date::new(2020, 1, 31);
617        let d2 = Date::new(2020, 7, 31);
618        let yf = DayCountConvention::Thirty360.year_fraction(d1, d2);
619        assert!((yf - 0.5).abs() < 1e-9);
620    }
621
622    #[test]
623    fn test_discount_factor_at_zero() {
624        let ts = sample_ts();
625        assert_eq!(ts.discount_factor_at_time(0.0), 1.0);
626    }
627
628    #[test]
629    fn test_validation_errors() {
630        let asof = Date::new(2024, 1, 1);
631        let d1 = asof.add_days(365);
632        let d2 = asof.add_days(730);
633        // Mismatched lengths
634        assert!(TermStructure::new(
635            vec![d1, d2], vec![0.95], DayCountConvention::Actual365,
636            InterpolationMethod::LogLinear, asof,
637        ).is_err());
638        // Discount factor > 1
639        assert!(TermStructure::new(
640            vec![d1, d2], vec![1.1, 0.9], DayCountConvention::Actual365,
641            InterpolationMethod::LogLinear, asof,
642        ).is_err());
643    }
644}