use crate::curves::Curve;
use crate::error::CurveError;
use crate::error::SurfaceError;
use crate::surfaces::Surface;
use positive::Positive;
#[cfg(test)]
use rust_decimal::MathematicalOps;
pub trait SmileDynamicsCurve {
fn smile_dynamics_curve(&self) -> Result<Curve, CurveError>;
}
pub trait SmileDynamicsSurface {
fn smile_dynamics_surface(
&self,
days_to_expiry: Vec<Positive>,
) -> Result<Surface, SurfaceError>;
}
#[cfg(test)]
mod tests_smile_dynamics {
use super::*;
use crate::curves::Point2D;
use crate::surfaces::Point3D;
use positive::pos_or_panic;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use std::collections::BTreeSet;
struct TestSmileDynamics {
underlying_price: Positive,
atm_vol: Positive,
skew: Decimal,
curvature: Decimal,
}
impl SmileDynamicsCurve for TestSmileDynamics {
fn smile_dynamics_curve(&self) -> Result<Curve, CurveError> {
let mut points = BTreeSet::new();
let spot = self.underlying_price.to_dec();
let atm_vol = self.atm_vol.to_dec();
let strikes = [
spot * dec!(0.85),
spot * dec!(0.90),
spot * dec!(0.95),
spot,
spot * dec!(1.05),
spot * dec!(1.10),
spot * dec!(1.15),
];
for strike in strikes {
let moneyness = (strike / spot).ln(); let iv = atm_vol + self.skew * moneyness + self.curvature * moneyness * moneyness;
points.insert(Point2D::new(strike, iv.max(dec!(0.01))));
}
Ok(Curve::new(points))
}
}
impl SmileDynamicsSurface for TestSmileDynamics {
fn smile_dynamics_surface(
&self,
days_to_expiry: Vec<Positive>,
) -> Result<Surface, SurfaceError> {
let mut points = BTreeSet::new();
let spot = self.underlying_price.to_dec();
let atm_vol = self.atm_vol.to_dec();
let strikes = [
spot * dec!(0.85),
spot * dec!(0.90),
spot * dec!(0.95),
spot,
spot * dec!(1.05),
spot * dec!(1.10),
spot * dec!(1.15),
];
for days in &days_to_expiry {
let time_factor = (days.to_dec() / dec!(30.0)).sqrt().unwrap_or(Decimal::ONE);
let adjusted_skew = self.skew / time_factor;
let adjusted_curvature = self.curvature / time_factor;
for strike in strikes {
let moneyness = (strike / spot).ln();
let iv = atm_vol
+ adjusted_skew * moneyness
+ adjusted_curvature * moneyness * moneyness;
points.insert(Point3D::new(strike, days.to_dec(), iv.max(dec!(0.01))));
}
}
Ok(Surface::new(points))
}
}
#[test]
fn test_smile_dynamics_curve_creation() {
let smile = TestSmileDynamics {
underlying_price: pos_or_panic!(450.0),
atm_vol: pos_or_panic!(0.20),
skew: dec!(-0.10), curvature: dec!(0.05), };
let curve = smile.smile_dynamics_curve();
assert!(curve.is_ok());
let curve = curve.unwrap();
assert_eq!(curve.points.len(), 7);
}
#[test]
fn test_smile_dynamics_curve_skew() {
let smile = TestSmileDynamics {
underlying_price: pos_or_panic!(450.0),
atm_vol: pos_or_panic!(0.20),
skew: dec!(-0.10), curvature: dec!(0.00), };
let curve = smile.smile_dynamics_curve().unwrap();
let points: Vec<&Point2D> = curve.points.iter().collect();
let low_strike_iv = points.first().map(|p| p.y).unwrap_or(Decimal::ZERO);
let high_strike_iv = points.last().map(|p| p.y).unwrap_or(Decimal::ZERO);
assert!(low_strike_iv > high_strike_iv);
}
#[test]
fn test_smile_dynamics_curve_curvature() {
let smile = TestSmileDynamics {
underlying_price: pos_or_panic!(450.0),
atm_vol: pos_or_panic!(0.20),
skew: dec!(0.00), curvature: dec!(0.10), };
let curve = smile.smile_dynamics_curve().unwrap();
let points: Vec<&Point2D> = curve.points.iter().collect();
let atm_point = points.iter().min_by(|a, b| {
let spot = dec!(450.0);
let a_dist = (a.x - spot).abs();
let b_dist = (b.x - spot).abs();
a_dist.partial_cmp(&b_dist).unwrap()
});
if let Some(atm) = atm_point {
for point in points.iter() {
assert!(point.y >= atm.y - dec!(0.001)); }
}
}
#[test]
fn test_smile_dynamics_surface_creation() {
let smile = TestSmileDynamics {
underlying_price: pos_or_panic!(450.0),
atm_vol: pos_or_panic!(0.20),
skew: dec!(-0.10),
curvature: dec!(0.05),
};
let days = vec![pos_or_panic!(7.0), pos_or_panic!(14.0), pos_or_panic!(30.0)];
let surface = smile.smile_dynamics_surface(days);
assert!(surface.is_ok());
let surface = surface.unwrap();
assert_eq!(surface.points.len(), 21);
}
#[test]
fn test_smile_dynamics_surface_term_structure() {
let smile = TestSmileDynamics {
underlying_price: pos_or_panic!(450.0),
atm_vol: pos_or_panic!(0.20),
skew: dec!(-0.10),
curvature: dec!(0.05),
};
let days = vec![pos_or_panic!(7.0), pos_or_panic!(30.0)];
let surface = smile.smile_dynamics_surface(days).unwrap();
let otm_strike = dec!(450.0) * dec!(0.90);
let points: Vec<&Point3D> = surface
.points
.iter()
.filter(|p| (p.x - otm_strike).abs() < dec!(1.0))
.collect();
let iv_7d = points.iter().find(|p| p.y == dec!(7.0)).map(|p| p.z);
let iv_30d = points.iter().find(|p| p.y == dec!(30.0)).map(|p| p.z);
if let (Some(iv7), Some(iv30)) = (iv_7d, iv_30d) {
assert!(iv7 >= iv30);
}
}
#[test]
fn test_smile_dynamics_surface_empty_days() {
let smile = TestSmileDynamics {
underlying_price: pos_or_panic!(450.0),
atm_vol: pos_or_panic!(0.20),
skew: dec!(-0.10),
curvature: dec!(0.05),
};
let days: Vec<Positive> = vec![];
let surface = smile.smile_dynamics_surface(days).unwrap();
assert!(surface.points.is_empty());
}
}