Heuristic:Avhz RustQuant Interpolation Method Selection
| Knowledge Sources | |
|---|---|
| Domains | Yield_Curves, Interpolation |
| Last Updated | 2026-02-07 19:00 GMT |
Overview
Choosing between Linear and Exponential interpolation for yield curves, with lazy Nelson-Siegel-Svensson fitting via particle swarm optimization for off-grid rate queries.
Description
RustQuant's `Curve` supports four interpolation methods via the `InterpolationMethod` enum: Linear (default), Exponential, CubicSpline (not yet implemented), and Lagrange (not yet implemented). Rate queries first check the node cache (`BTreeMap<Date, f64>`); if the date is not present, the interpolator backend is used. The curve also supports lazy NSS fitting via particle swarm optimization with 1000 particles and 69 iterations, triggered only when off-grid rates are needed. The loss function is log-cosh rather than MSE or MAE.
Usage
Apply this heuristic when constructing a `Curve` via `Curve::new()`. Choose the interpolation method based on the expected curve shape and downstream use case.
The Insight (Rule of Thumb)
- Linear (default): Use for quick prototyping and when derivatives of the curve are not needed. Fast computation, but produces discontinuous first derivatives at node points.
- Exponential: Use for discount curves where rates decay exponentially. Captures the natural shape of discount factors better than linear interpolation. Produces smoother curves but is still piecewise.
- CubicSpline / Lagrange: Not yet implemented (`todo!()` in source). Do not use.
- NSS Fitting: The `fit_nss()` method uses particle swarm optimization with:
- 1000 particles (~167 per parameter for 6 NSS parameters)
- 69 iterations (empirical convergence point)
- log-cosh loss (robust to outliers, smoother than MAE, less sensitive than MSE)
- Parameter bounds: Beta_0 in (epsilon, 0.3), Beta_1 in (-0.3, 0.3), Beta_2/3 in (-1.0, 1.0), Tau_1/2 in (epsilon, 5.0)
- Trade-off: NSS fitting is expensive (1000 particles x 69 iterations). Use lazy fitting to defer cost until needed. Cache results via `get_rate_and_insert()`.
Reasoning
The log-cosh loss function is used because it combines the best properties of MAE and MSE:
- For small errors, `ln(cosh(x)) ~ x^2/2` (behaves like MSE, smooth gradients)
- For large errors, `ln(cosh(x)) ~ |x| - ln(2)` (behaves like MAE, robust to outliers)
- It is twice differentiable everywhere, which helps optimization convergence
The particle swarm parameters (1000 particles, 69 iterations) represent an empirical balance: 6 NSS parameters need sufficient exploration (~150-200 particles per dimension) while keeping computation tractable. The commented-out inertia/cognitive/social factors suggest these defaults were tuned iteratively.
Code Evidence
Lazy rate lookup with interpolation fallback from `curves.rs:175-179`:
pub fn get_rate(&self, date: Date) -> Option<f64> {
match self.nodes.get(&date) {
Some(rate) => Some(*rate),
None => self.interpolator.interpolate(date).ok(),
}
}
PSO configuration from `curves.rs:345-346`:
const CURVE_OPTIM_MAX_ITER: u64 = 69;
const CURVE_OPTIM_SWARM_SIZE: usize = 1000;
NSS parameter bounds from `curves.rs:350-357`:
let bounds = [
(zero, 0.3), // Beta_0
(-0.3, 0.3), // Beta_1
(-1.0, 1.0), // Beta_2
(-1.0, 1.0), // Beta_3
(zero, 5.0), // Tau_1
(zero, 5.0), // Tau_2
]
Log-cosh loss function from `curves.rs:336`:
let log_cosh_loss = data.map(|(o, p)| (p - o).cosh().ln()).sum::<f64>() / n;
Unimplemented methods from `curves.rs:119-124`:
InterpolationMethod::CubicSpline => {
todo!("Implement CubicSplineInterpolator")
}
InterpolationMethod::Lagrange => {
todo!("Implement LagrangeInterpolator")
}