use crate::calibration::CalibrationResult;
use crate::errors::CalibrationError;
use crate::math::{nelder_mead, solve_spd_3};
use crate::raw::RawSvi;
use crate::types::Quote;
const MIN_QUOTES: usize = 5;
const OUTER_TOL: f64 = 1e-12;
const OUTER_MAX_ITER: usize = 2000;
pub fn calibrate(quotes: &[Quote]) -> Result<CalibrationResult, CalibrationError> {
if quotes.is_empty() {
return Err(CalibrationError::EmptyQuotes);
}
if quotes.len() < MIN_QUOTES {
return Err(CalibrationError::TooFewQuotes {
got: quotes.len(),
need: MIN_QUOTES,
});
}
if quotes.iter().all(|q| q.weight <= 0.0) {
return Err(CalibrationError::AllWeightsZero);
}
let k_min = quotes.iter().map(|q| q.k).fold(f64::INFINITY, f64::min);
let k_max = quotes.iter().map(|q| q.k).fold(f64::NEG_INFINITY, f64::max);
let k_span = (k_max - k_min).max(1e-3);
let w_max = quotes
.iter()
.map(|q| q.w)
.fold(0.0_f64, f64::max)
.max(1e-12);
let outer = |p: &[f64]| -> f64 {
let m = p[0];
let sigma = p[1].exp();
if !m.is_finite() || !sigma.is_finite() || sigma <= 0.0 {
return f64::INFINITY;
}
inner_solve(quotes, m, sigma, w_max).0
};
let m_seeds = [
k_min,
0.5 * (k_min + k_max),
k_max,
k_min - 0.25 * k_span,
k_max + 0.25 * k_span,
];
let sigma_seeds = [0.1 * k_span, 0.3 * k_span, k_span, 2.0 * k_span];
let mut best_obj = f64::INFINITY;
let mut best_m = 0.5 * (k_min + k_max);
let mut best_sigma = 0.3 * k_span;
for &m0 in &m_seeds {
for &s0 in &sigma_seeds {
let start = [m0, s0.max(1e-6).ln()];
let res = nelder_mead(outer, &start, OUTER_TOL, OUTER_MAX_ITER);
if res.fx < best_obj {
best_obj = res.fx;
best_m = res.x[0];
best_sigma = res.x[1].exp();
}
}
}
let (resid, a, d, c) = inner_solve(quotes, best_m, best_sigma, w_max);
let b = if best_sigma > 0.0 {
c / best_sigma
} else {
0.0
};
let rho = if c.abs() > 1e-300 {
(d / c).clamp(-0.999_999, 0.999_999)
} else {
0.0
};
let total_weight: f64 = quotes.iter().map(|q| q.weight).sum();
let rmse = if total_weight > 0.0 {
(resid / total_weight).sqrt()
} else {
0.0
};
let slice = RawSvi::new(a, b, rho, best_m, best_sigma).map_err(CalibrationError::Param)?;
Ok(CalibrationResult::new(slice, rmse))
}
#[allow(clippy::too_many_lines)]
fn inner_solve(quotes: &[Quote], m: f64, sigma: f64, w_max: f64) -> (f64, f64, f64, f64) {
let mut rows: Vec<([f64; 3], f64, f64)> = Vec::with_capacity(quotes.len());
for q in quotes {
if q.weight <= 0.0 {
continue;
}
let y = (q.k - m) / sigma;
let z = (y * y + 1.0).sqrt();
rows.push(([1.0, y, z], q.w, q.weight));
}
if rows.is_empty() {
return (f64::INFINITY, 0.0, 0.0, 0.0);
}
let mut a_mat = [0.0_f64; 6]; let mut rhs = [0.0_f64; 3];
for (phi, w, weight) in &rows {
let ww = *weight;
a_mat[0] += ww * phi[0] * phi[0];
a_mat[1] += ww * phi[0] * phi[1];
a_mat[2] += ww * phi[0] * phi[2];
a_mat[3] += ww * phi[1] * phi[1];
a_mat[4] += ww * phi[1] * phi[2];
a_mat[5] += ww * phi[2] * phi[2];
rhs[0] += ww * phi[0] * w;
rhs[1] += ww * phi[1] * w;
rhs[2] += ww * phi[2] * w;
}
let residual_of = |a: f64, d: f64, c: f64| -> f64 {
rows.iter()
.map(|(phi, w, weight)| {
let model = a * phi[0] + d * phi[1] + c * phi[2];
let r = model - w;
weight * r * r
})
.sum()
};
let c_hi = 4.0 * sigma;
let feasible = |a: f64, d: f64, c: f64| -> bool {
let eps = 1e-9 * (1.0 + c_hi + w_max);
a >= -eps
&& a <= w_max + eps
&& c >= -eps
&& c <= c_hi + eps
&& d.abs() <= c + eps
&& d.abs() <= c_hi - c + eps
};
let mut best_resid = f64::INFINITY;
let mut best = (0.0_f64, 0.0_f64, 0.0_f64);
let mut consider = |a: f64, d: f64, c: f64| {
if a.is_finite() && d.is_finite() && c.is_finite() && feasible(a, d, c) {
let r = residual_of(a, d, c);
if r < best_resid {
best_resid = r;
best = (a, d, c);
}
}
};
if let Some(x) = solve_spd_3(&a_mat, &rhs) {
consider(x[0], x[1], x[2]);
}
for &a_fix in &[0.0, w_max] {
if let Some((d, c)) = solve_2x2(
a_mat[3],
a_mat[4],
a_mat[5],
rhs[1] - a_mat[1] * a_fix,
rhs[2] - a_mat[2] * a_fix,
) {
consider(a_fix, d, c);
}
}
for &c_fix in &[0.0, c_hi] {
if let Some((a, d)) = solve_2x2(
a_mat[0],
a_mat[1],
a_mat[3],
rhs[0] - a_mat[2] * c_fix,
rhs[1] - a_mat[4] * c_fix,
) {
consider(a, d, c_fix);
}
}
for &s in &[1.0_f64, -1.0] {
let a00 = a_mat[0];
let a01 = s * a_mat[1] + a_mat[2];
let a11 = a_mat[5] + 2.0 * s * a_mat[4] + a_mat[3];
let r0 = rhs[0];
let r1 = s * rhs[1] + rhs[2];
if let Some((a, c)) = solve_2x2(a00, a01, a11, r0, r1) {
consider(a, s * c, c);
}
}
for &s in &[1.0_f64, -1.0] {
let a00 = a_mat[0];
let a01 = a_mat[2] - s * a_mat[1];
let a11 = a_mat[5] - 2.0 * s * a_mat[4] + a_mat[3];
let r0 = rhs[0] - s * c_hi * a_mat[1];
let r1 = (rhs[2] - s * rhs[1]) - s * c_hi * (a_mat[4] - s * a_mat[3]);
if let Some((a, c)) = solve_2x2(a00, a01, a11, r0, r1) {
consider(a, s * c_hi - s * c, c);
}
}
for &a_fix in &[0.0, w_max] {
for &c_fix in &[0.0, c_hi] {
let denom = a_mat[3];
if denom > 0.0 {
let d = (rhs[1] - a_mat[1] * a_fix - a_mat[4] * c_fix) / denom;
consider(a_fix, d, c_fix);
}
}
for &s in &[1.0_f64, -1.0] {
let a11 = a_mat[5] + 2.0 * s * a_mat[4] + a_mat[3];
if a11 > 0.0 {
let c = (s * (rhs[1] - a_mat[1] * a_fix) + (rhs[2] - a_mat[2] * a_fix)) / a11;
consider(a_fix, s * c, c);
}
let a11b = a_mat[5] - 2.0 * s * a_mat[4] + a_mat[3];
if a11b > 0.0 {
let r = (rhs[2] - a_mat[2] * a_fix)
- s * (rhs[1] - a_mat[1] * a_fix)
- s * c_hi * (a_mat[4] - s * a_mat[3]);
let c = r / a11b;
consider(a_fix, s * c_hi - s * c, c);
}
}
}
let half = c_hi / 2.0;
let dc_vertices = [(0.0, 0.0), (0.0, c_hi), (half, half), (-half, half)];
for &a_fix in &[0.0, w_max] {
for &(d, c) in &dc_vertices {
consider(a_fix, d, c);
}
}
for &(d, c) in &dc_vertices {
if a_mat[0] > 0.0 {
let a = (rhs[0] - a_mat[1] * d - a_mat[2] * c) / a_mat[0];
consider(a, d, c);
}
}
(best_resid, best.0, best.1, best.2)
}
fn solve_2x2(a00: f64, a01: f64, a11: f64, r0: f64, r1: f64) -> Option<(f64, f64)> {
let det = a00 * a11 - a01 * a01;
if det.abs() < 1e-300 || !det.is_finite() {
return None;
}
let x0 = (a11 * r0 - a01 * r1) / det;
let x1 = (a00 * r1 - a01 * r0) / det;
if x0.is_finite() && x1.is_finite() {
Some((x0, x1))
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic(svi: &RawSvi, ks: &[f64]) -> Vec<Quote> {
ks.iter()
.map(|&k| Quote::new(k, svi.total_variance(k), 1.0).unwrap())
.collect()
}
#[test]
fn rejects_empty() {
assert!(matches!(calibrate(&[]), Err(CalibrationError::EmptyQuotes)));
}
#[test]
fn rejects_too_few_quotes() {
let q = Quote::new(0.0, 0.04, 1.0).unwrap();
assert!(matches!(
calibrate(&[q, q, q]),
Err(CalibrationError::TooFewQuotes { .. })
));
}
#[test]
fn rejects_all_zero_weights() {
let quotes: Vec<Quote> = (0..6)
.map(|i| Quote::new(f64::from(i) * 0.1 - 0.3, 0.04, 0.0).unwrap())
.collect();
assert!(matches!(
calibrate("es),
Err(CalibrationError::AllWeightsZero)
));
}
#[test]
fn recovers_synthetic_parameters() {
let truth = RawSvi::new(0.04, 0.4, -0.3, 0.05, 0.15).unwrap();
let ks = [-0.4, -0.25, -0.1, 0.0, 0.1, 0.25, 0.4];
let quotes = synthetic(&truth, &ks);
let fit = calibrate("es).unwrap();
assert!(fit.rmse < 1e-5, "rmse = {}", fit.rmse);
for &k in &[-0.6, -0.2, 0.0, 0.2, 0.6] {
let err = (fit.slice.total_variance(k) - truth.total_variance(k)).abs();
assert!(err < 1e-4, "k = {k}, err = {err}");
}
}
#[test]
fn recovers_symmetric_smile() {
let truth = RawSvi::new(0.03, 0.3, 0.0, 0.0, 0.2).unwrap();
let ks = [-0.5, -0.3, -0.1, 0.0, 0.1, 0.3, 0.5];
let quotes = synthetic(&truth, &ks);
let fit = calibrate("es).unwrap();
assert!(fit.rmse < 1e-5, "rmse = {}", fit.rmse);
assert!(fit.slice.rho.abs() < 1e-2, "rho = {}", fit.slice.rho);
}
#[test]
fn recovers_positive_skew() {
let truth = RawSvi::new(0.05, 0.35, 0.4, -0.1, 0.18).unwrap();
let ks = [-0.4, -0.2, -0.05, 0.05, 0.2, 0.4, 0.6];
let quotes = synthetic(&truth, &ks);
let fit = calibrate("es).unwrap();
assert!(fit.rmse < 1e-4, "rmse = {}", fit.rmse);
for &k in &[-0.3, 0.0, 0.3] {
let err = (fit.slice.total_variance(k) - truth.total_variance(k)).abs();
assert!(err < 1e-3, "k = {k}, err = {err}");
}
}
#[test]
fn graceful_degradation_with_noise() {
let truth = RawSvi::new(0.04, 0.4, -0.3, 0.05, 0.15).unwrap();
let ks = [-0.4, -0.25, -0.1, 0.0, 0.1, 0.25, 0.4];
let mut state = 12_345_u64;
let quotes: Vec<Quote> = ks
.iter()
.map(|&k| {
state = state
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
let noise =
(f64::from((state >> 40) as u32) / f64::from(u32::MAX) - 0.5) * 2.0 * 5e-4;
Quote::new(k, truth.total_variance(k) + noise, 1.0).unwrap()
})
.collect();
let fit = calibrate("es).unwrap();
assert!(fit.rmse < 1e-2, "rmse = {}", fit.rmse);
let atm_err = (fit.slice.total_variance(0.0) - truth.total_variance(0.0)).abs();
assert!(atm_err < 5e-3, "atm err = {atm_err}");
}
#[test]
fn solve_2x2_identity() {
let (x, y) = solve_2x2(1.0, 0.0, 1.0, 3.0, 7.0).unwrap();
assert!((x - 3.0).abs() < 1e-15);
assert!((y - 7.0).abs() < 1e-15);
}
#[test]
fn solve_2x2_rejects_singular() {
assert!(solve_2x2(1.0, 1.0, 1.0, 1.0, 1.0).is_none());
}
}