use crate::errors::ParamError;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Quote {
pub k: f64,
pub w: f64,
pub weight: f64,
}
impl Quote {
pub fn new(k: f64, w: f64, weight: f64) -> Result<Self, ParamError> {
if !k.is_finite() {
return Err(ParamError::NonFinite { name: "k" });
}
if !w.is_finite() {
return Err(ParamError::NonFinite { name: "w" });
}
if !weight.is_finite() {
return Err(ParamError::NonFinite { name: "weight" });
}
if w < 0.0 {
return Err(ParamError::NegativeTotalVariance { w });
}
if weight < 0.0 {
return Err(ParamError::NegativeWeight { weight });
}
Ok(Self { k, w, weight })
}
#[must_use]
pub const fn new_unchecked(k: f64, w: f64, weight: f64) -> Self {
Self { k, w, weight }
}
pub fn implied_vol(&self, t: f64) -> Result<f64, ParamError> {
if t <= 0.0 || !t.is_finite() {
return Err(ParamError::NonPositiveMaturity { t });
}
Ok((self.w / t).sqrt())
}
}
pub fn quotes_from_triples(triples: &[(f64, f64, f64)]) -> Result<Vec<Quote>, ParamError> {
triples
.iter()
.map(|&(k, w, weight)| Quote::new(k, w, weight))
.collect()
}
#[must_use]
#[inline]
pub fn total_variance_from_vol(vol: f64, t: f64) -> f64 {
vol * vol * t
}
#[must_use]
#[inline]
pub fn log_moneyness(strike: f64, forward: f64) -> f64 {
(strike / forward).ln()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::errors::ParamError;
#[test]
fn quote_new_valid() {
let q = Quote::new(-0.1, 0.0432, 2.0).unwrap();
assert!((q.k + 0.1).abs() < 1e-15);
assert!((q.w - 0.0432).abs() < 1e-15);
assert!((q.weight - 2.0).abs() < 1e-15);
}
#[test]
fn quote_new_rejects_negative_variance() {
assert_eq!(
Quote::new(0.0, -0.01, 1.0),
Err(ParamError::NegativeTotalVariance { w: -0.01 })
);
}
#[test]
fn quote_new_rejects_negative_weight() {
assert_eq!(
Quote::new(0.0, 0.04, -1.0),
Err(ParamError::NegativeWeight { weight: -1.0 })
);
}
#[test]
fn quote_new_rejects_non_finite() {
assert_eq!(
Quote::new(f64::NAN, 0.04, 1.0),
Err(ParamError::NonFinite { name: "k" })
);
assert_eq!(
Quote::new(0.0, f64::INFINITY, 1.0),
Err(ParamError::NonFinite { name: "w" })
);
assert_eq!(
Quote::new(0.0, 0.04, f64::NAN),
Err(ParamError::NonFinite { name: "weight" })
);
}
#[test]
fn quote_new_unchecked() {
let q = Quote::new_unchecked(0.1, 0.05, 0.5);
assert!((q.k - 0.1).abs() < 1e-15);
}
#[test]
fn quote_implied_vol_roundtrip() {
let q = Quote::new(0.0, 0.09, 1.0).unwrap();
let vol = q.implied_vol(1.0).unwrap();
assert!((vol - 0.30).abs() < 1e-12);
}
#[test]
fn quote_implied_vol_rejects_bad_maturity() {
let q = Quote::new(0.0, 0.04, 1.0).unwrap();
assert!(matches!(
q.implied_vol(0.0),
Err(ParamError::NonPositiveMaturity { .. })
));
assert!(matches!(
q.implied_vol(-1.0),
Err(ParamError::NonPositiveMaturity { .. })
));
}
#[test]
fn quotes_from_triples_builds_slice() {
let slice = quotes_from_triples(&[(-0.1, 0.05, 1.0), (0.1, 0.05, 1.0)]).unwrap();
assert_eq!(slice.len(), 2);
}
#[test]
fn quotes_from_triples_propagates_error() {
let bad = quotes_from_triples(&[(0.0, -1.0, 1.0)]);
assert!(bad.is_err());
}
#[test]
fn total_variance_from_vol_works() {
assert!((total_variance_from_vol(0.20, 2.0) - 0.08).abs() < 1e-15);
}
#[test]
fn log_moneyness_atm_is_zero() {
assert!(log_moneyness(100.0, 100.0).abs() < 1e-15);
}
#[test]
fn quote_is_copy() {
let q = Quote::new(0.0, 0.04, 1.0).unwrap();
let copy = q;
assert_eq!(q, copy);
}
}