libitofin 0.44.0

A ground-up Rust port of QuantLib: quantitative-finance primitives for pricing, risk, and numerical methods.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
//! Discrete time grid.
//!
//! Port of `ql/timegrid.{hpp,cpp}`, restricted to the regularly spaced grid used
//! by the Monte Carlo path generators. [`TimeGrid::new`] mirrors
//! `TimeGrid(Time end, Size steps)` (`timegrid.cpp:26`).
//!
//! Divergences from QuantLib, all deliberate:
//! - **`steps == 0`**: C++ does not guard this; `dt = end/0` is `+inf` and the
//!   grid collapses to a single `NaN` point. Per D10 (usability at boundaries)
//!   we return `Err("at least one step required")` instead of a poisoned grid.
//! - **`front`/`back`/`at`**: return `Option<Time>` rather than a bare `Time`,
//!   matching the `first`/`last` slice mapping already used by
//!   [`Array`](crate::math::array::Array); a [`Default`] (empty) grid is
//!   representable, so bare accessors would panic. `Index`/[`dt`](TimeGrid::dt)
//!   stay unchecked, mirroring C++'s unchecked `operator[]`/`dt`.
//! - **storage**: `Vec<Time>` rather than `Array`; the grid needs no
//!   element-wise math, only sequence access.
//!
//! The mandatory-times ctor (`timegrid.hpp:87`, [`with_mandatory_times`]) places a
//! caller-supplied set of times on the grid verbatim and fills regularly spaced
//! interior points between them; the lattice engines need it so swap reset/pay
//! times land on exact grid nodes.
//!
//! The value-semantics mandatory-times constructor preserves only supplied
//! nodes and an initial zero. Initializer-list conveniences and `closest_time`
//! remain unexposed.

use std::ops::Index;

use crate::errors::QlResult;
use crate::math::comparison::close_enough;
use crate::types::{Real, Size, Time};
use crate::{fail, require};

/// A discrete, regularly spaced grid of times starting at zero.
#[derive(Clone, Debug, PartialEq, Default)]
pub struct TimeGrid {
    times: Vec<Time>,
    dt: Vec<Time>,
    mandatory_times: Vec<Time>,
}

impl TimeGrid {
    /// Builds an exact grid from mandatory times, sorting and removing duplicates.
    /// Zero is prepended when absent, without inserting intermediate nodes.
    ///
    /// # Errors
    /// Rejects empty, negative or non-finite times and a grid without a positive end.
    pub fn from_mandatory_times(times: &[Time]) -> QlResult<Self> {
        require!(!times.is_empty(), "empty time sequence");
        require!(
            times.iter().all(|t| t.is_finite() && *t >= 0.0),
            "invalid grid time"
        );
        let mut mandatory_times = times.to_vec();
        mandatory_times.sort_by(f64::total_cmp);
        mandatory_times.dedup_by(|a, b| close_enough(*a, *b));
        require!(
            mandatory_times.last().is_some_and(|t| *t > 0.0),
            "positive grid end required"
        );
        let mut times = mandatory_times.clone();
        if times[0] > 0.0 {
            times.insert(0, 0.0);
        }
        let dt = times.windows(2).map(|p| p[1] - p[0]).collect();
        Ok(Self {
            times,
            dt,
            mandatory_times,
        })
    }

    /// Regularly spaced grid: `steps + 1` points `0, dt, 2*dt, ..., end` with
    /// `dt = end / steps`.
    ///
    /// Mirrors `TimeGrid(Time end, Size steps)` (`timegrid.cpp:26`). Points are
    /// built by multiplication (`dt * i`) rather than running accumulation, as
    /// in C++, to preserve exactness at the endpoints.
    ///
    /// # Errors
    /// Returns `Err` if `end <= 0.0` ("negative times not allowed", the C++
    /// message) or if `steps == 0` (see the module divergence note).
    #[allow(clippy::neg_cmp_op_on_partial_ord)]
    pub fn new(end: Time, steps: Size) -> QlResult<Self> {
        require!(end > 0.0, "negative times not allowed");
        require!(steps > 0, "at least one step required");

        let dt = end / steps as Real;
        let times = (0..=steps).map(|i| dt * i as Real).collect();
        Ok(TimeGrid {
            times,
            dt: vec![dt; steps],
            mandatory_times: vec![end],
        })
    }

    /// Grid through a set of mandatory times, with regularly spaced interior
    /// points (`timegrid.hpp:87`, the `(begin, end, steps)` ctor).
    ///
    /// The sorted, `close_enough`-deduped `times` become grid nodes *verbatim*
    /// (so a later `index`/`initialize` resolves them exactly); between each
    /// consecutive pair the grid inserts `round((gap)/dt_max)` (at least one)
    /// evenly spaced points, and a leading `0.0` is prepended when the first
    /// mandatory time is positive. `dt_` is the adjacent differences of the
    /// resulting nodes (unlike the regular [`new`](TimeGrid::new) grid, whose
    /// `dt` is uniform).
    ///
    /// `dt_max` is `last / steps` when `steps > 0`; when `steps == 0` it is the
    /// smallest gap between distinct mandatory times (the C++ auto-spacing
    /// branch). Note the semantic split from [`new`](TimeGrid::new), where
    /// `steps == 0` is an error: here it selects auto-spacing, faithful to the
    /// C++ overload that takes `steps` and special-cases `0`.
    ///
    /// # Errors
    /// Returns `Err` on an empty `times`, a negative first time, or (in the
    /// `steps == 0` branch) fewer than two distinct points to infer a spacing
    /// from.
    #[allow(clippy::float_cmp, clippy::neg_cmp_op_on_partial_ord)]
    pub fn with_mandatory_times(times: &[Time], steps: Size) -> QlResult<Self> {
        require!(!times.is_empty(), "empty time sequence");
        let mut mandatory = times.to_vec();
        mandatory.sort_by(|a, b| {
            a.partial_cmp(b)
                .expect("time-grid mandatory times must be totally ordered")
        });
        require!(mandatory[0] >= 0.0, "negative times not allowed");
        mandatory.dedup_by(|a, b| close_enough(*a, *b));

        let last = *mandatory
            .last()
            .expect("mandatory is non-empty after the guard");
        let dt_max = if steps == 0 {
            let mut diff = Vec::with_capacity(mandatory.len());
            diff.push(mandatory[0]);
            for pair in mandatory.windows(2) {
                diff.push(pair[1] - pair[0]);
            }
            require!(
                !diff.is_empty(),
                "at least two distinct points required in time grid"
            );
            if diff.first() == Some(&0.0) {
                diff.remove(0);
            }
            require!(!diff.is_empty(), "not enough distinct points in time grid");
            diff.into_iter().fold(Real::INFINITY, Real::min)
        } else {
            last / steps as Real
        };

        let mut grid_times = vec![0.0];
        let mut period_begin = 0.0;
        for &period_end in &mandatory {
            if period_end != 0.0 {
                let n_steps = (((period_end - period_begin) / dt_max).round()).max(1.0) as Size;
                let dt = (period_end - period_begin) / n_steps as Real;
                for n in 1..=n_steps {
                    grid_times.push(period_begin + n as Real * dt);
                }
            }
            period_begin = period_end;
        }

        let dt = grid_times.windows(2).map(|w| w[1] - w[0]).collect();
        Ok(TimeGrid {
            times: grid_times,
            dt,
            mandatory_times: mandatory,
        })
    }

    /// The spacing `dt_[i]` at step `i` (`timegrid.hpp:159`). Unchecked, like
    /// C++'s `dt`: panics if `i` is out of range.
    pub fn dt(&self, i: Size) -> Time {
        self.dt[i]
    }

    /// The mandatory time points guaranteed to lie on the grid
    /// (`timegrid.hpp:156`). For a regular grid this is `[end]`.
    pub fn mandatory_times(&self) -> &[Time] {
        &self.mandatory_times
    }

    /// The grid points as a slice, covering C++'s `begin`/`end` iteration
    /// (`timegrid.hpp:171`): iterate with `grid.times().iter()`.
    pub fn times(&self) -> &[Time] {
        &self.times
    }

    /// The number of grid points (`timegrid.hpp:169`).
    pub fn size(&self) -> Size {
        self.times.len()
    }

    /// Whether the grid has no points (`timegrid.hpp:170`).
    pub fn empty(&self) -> bool {
        self.times.is_empty()
    }

    /// The bounds-checked point at index `i`, mirroring C++'s `at`
    /// (`timegrid.hpp:168`), but returning `None` rather than throwing.
    pub fn at(&self, i: Size) -> Option<Time> {
        self.times.get(i).copied()
    }

    /// The index `i` such that `grid[i]` is closest to `t` (`timegrid.cpp:80`).
    ///
    /// Mirrors C++'s `std::lower_bound` walk: find the first node `>= t`, then
    /// return whichever of it and its predecessor is nearer, clamping at the
    /// ends. On an empty grid this returns `0`, matching C++'s
    /// `begin == end` short-circuit (there `size()-1` underflows, so callers of
    /// [`index`](TimeGrid::index) never reach it on an empty grid).
    pub fn closest_index(&self, t: Time) -> Size {
        let result = self.times.partition_point(|&x| x < t);
        if result == 0 {
            0
        } else if result == self.times.len() {
            self.times.len() - 1
        } else {
            let dt1 = self.times[result] - t;
            let dt2 = t - self.times[result - 1];
            if dt1 < dt2 { result } else { result - 1 }
        }
    }

    /// The index `i` such that `grid[i] == t` (`timegrid.cpp:43`).
    ///
    /// Finds the [`closest_index`](TimeGrid::closest_index) and requires it to
    /// coincide with `t` under [`close_enough`]; otherwise the grid cannot
    /// resolve `t` to a node and this returns `Err` (C++ `QL_FAIL`, split into
    /// three messages there, folded into one here per D4).
    ///
    /// # Errors
    /// Returns `Err` when no grid node is [`close_enough`] to `t`.
    pub fn index(&self, t: Time) -> QlResult<Size> {
        let i = self.closest_index(t);
        if close_enough(t, self.times[i]) {
            Ok(i)
        } else {
            fail!(
                "using inadequate time grid: no node is close enough to the \
                 required time t = {t} (closest node is t1 = {})",
                self.times[i]
            );
        }
    }

    /// The first grid point (`timegrid.hpp:175`), or `None` if empty.
    pub fn front(&self) -> Option<Time> {
        self.times.first().copied()
    }

    /// The last grid point (`timegrid.hpp:176`), or `None` if empty.
    pub fn back(&self) -> Option<Time> {
        self.times.last().copied()
    }
}

/// Unchecked point access, mirroring C++'s `operator[]` (`timegrid.hpp:167`):
/// panics if `i` is out of range.
impl Index<Size> for TimeGrid {
    type Output = Time;

    fn index(&self, i: Size) -> &Time {
        &self.times[i]
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn regular_grid_matches_reference() {
        // Oracle: TimeGrid(1.0, 4) -> 5 points, uniform 0.25 spacing.
        let grid = TimeGrid::new(1.0, 4).unwrap();
        assert_eq!(grid.size(), 5);
        assert_eq!(grid.times(), &[0.0, 0.25, 0.5, 0.75, 1.0]);
        for i in 0..4 {
            assert_eq!(grid.dt(i), 0.25);
        }
        assert_eq!(grid.front(), Some(0.0));
        assert_eq!(grid.back(), Some(1.0));
        assert_eq!(grid.mandatory_times(), &[1.0]);
    }

    #[test]
    fn indexing_and_bounds_checks() {
        let grid = TimeGrid::new(1.0, 4).unwrap();
        assert_eq!(grid[2], 0.5);
        assert_eq!(grid.at(4), Some(1.0));
        assert_eq!(grid.at(5), None);
        assert!(!grid.empty());
    }

    #[test]
    fn default_grid_is_empty() {
        let grid = TimeGrid::default();
        assert!(grid.empty());
        assert_eq!(grid.size(), 0);
        assert_eq!(grid.front(), None);
        assert_eq!(grid.back(), None);
    }

    #[test]
    fn non_positive_end_is_rejected() {
        // C++ QL_REQUIRE(end > 0.0, "negative times not allowed").
        let err = TimeGrid::new(0.0, 4).unwrap_err();
        assert_eq!(err.message(), "negative times not allowed");
        assert!(TimeGrid::new(-1.0, 4).is_err());
    }

    #[test]
    fn zero_steps_is_rejected() {
        // Divergence from C++: guard the inf/NaN grid at the boundary.
        let err = TimeGrid::new(1.0, 0).unwrap_err();
        assert_eq!(err.message(), "at least one step required");
    }

    #[test]
    fn index_resolves_grid_aligned_times() {
        // timegrid.cpp:43: index(t) returns i with grid[i] == t.
        let grid = TimeGrid::new(1.0, 4).unwrap();
        assert_eq!(grid.index(0.0).unwrap(), 0);
        assert_eq!(grid.index(0.25).unwrap(), 1);
        assert_eq!(grid.index(0.5).unwrap(), 2);
        assert_eq!(grid.index(1.0).unwrap(), 4);
    }

    #[test]
    fn index_rejects_off_grid_times() {
        // timegrid.cpp:47: a t between nodes resolves to no index.
        let grid = TimeGrid::new(1.0, 4).unwrap();
        assert!(grid.index(0.3).is_err());
        assert!(grid.index(1.5).is_err());
        assert!(grid.index(-0.1).is_err());
    }

    #[test]
    fn with_mandatory_times_places_every_time_on_an_exact_node() {
        // timegrid.hpp:87: the mandatory times are grid nodes verbatim, so
        // index() resolves each one exactly. Unsorted, duplicated input is
        // sorted and close_enough-deduped first.
        let grid = TimeGrid::with_mandatory_times(&[1.0, 0.5, 1.0], 2).unwrap();
        assert_eq!(grid.times(), &[0.0, 0.5, 1.0]);
        assert_eq!(grid.mandatory_times(), &[0.5, 1.0]);
        // Each mandatory time is an EXACT node (==), not merely close.
        assert_eq!(grid[grid.index(0.5).unwrap()], 0.5);
        assert_eq!(grid[grid.index(1.0).unwrap()], 1.0);
    }

    #[test]
    fn with_mandatory_times_inserts_regular_interior_points() {
        // Between 1.0 and 3.0 with dt_max = 3/4 = 0.75, round(2/0.75) = 3 evenly
        // spaced points are inserted; the mandatory times stay exact nodes.
        let grid = TimeGrid::with_mandatory_times(&[1.0, 3.0], 4).unwrap();
        assert_eq!(grid.size(), 5);
        assert_eq!(grid[1], 1.0, "first mandatory time is a verbatim node");
        assert_eq!(
            grid.back(),
            Some(3.0),
            "last mandatory time is the final node"
        );
        assert_eq!(grid.mandatory_times(), &[1.0, 3.0]);
        // dt_ is the adjacent differences of the nodes, not a uniform spacing.
        assert!((grid.dt(1) - 2.0 / 3.0).abs() < 1e-15);
    }

    #[test]
    fn with_mandatory_times_auto_spaces_when_steps_is_zero() {
        // Divergence from `new`, where steps == 0 is an error: here it selects
        // the C++ auto-spacing branch, dt_max = the smallest gap between distinct
        // mandatory times (min(1, 1, 2) = 1 here).
        let grid = TimeGrid::with_mandatory_times(&[1.0, 2.0, 4.0], 0).unwrap();
        assert_eq!(grid.times(), &[0.0, 1.0, 2.0, 3.0, 4.0]);
        assert_eq!(grid.mandatory_times(), &[1.0, 2.0, 4.0]);
    }

    #[test]
    fn with_mandatory_times_rejects_empty_and_negative() {
        assert_eq!(
            TimeGrid::with_mandatory_times(&[], 4)
                .unwrap_err()
                .message(),
            "empty time sequence"
        );
        assert_eq!(
            TimeGrid::with_mandatory_times(&[-1.0, 2.0], 4)
                .unwrap_err()
                .message(),
            "negative times not allowed"
        );
    }

    #[test]
    fn closest_index_snaps_to_nearest_node() {
        // timegrid.cpp:80: nearest node, ends clamped, ties to the lower node.
        let grid = TimeGrid::new(1.0, 4).unwrap();
        assert_eq!(grid.closest_index(0.3), 1);
        assert_eq!(grid.closest_index(0.4), 2);
        assert_eq!(grid.closest_index(2.0), 4);
        assert_eq!(grid.closest_index(-1.0), 0);
        // A midpoint tie: dt1 (0.25 - 0.125) == dt2, so the lower node wins.
        assert_eq!(grid.closest_index(0.125), 0);
    }
}

#[cfg(test)]
mod exact_grid_tests {
    use super::*;
    #[test]
    fn exact_grid_keeps_nodes_and_rejects_invalid_times() {
        let grid = TimeGrid::from_mandatory_times(&[100.0, 1e-12, 100.0]).unwrap();
        assert_eq!(grid.times(), &[0.0, 1e-12, 100.0]);
        for times in [
            &[][..],
            &[0.0][..],
            &[f64::NAN, 1.0][..],
            &[f64::INFINITY][..],
            &[-1.0, 1.0][..],
        ] {
            assert!(TimeGrid::from_mandatory_times(times).is_err());
        }
    }
}