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 DeltaGammaProfileCurve {
fn delta_gamma_curve(&self) -> Result<Curve, CurveError>;
}
pub trait DeltaGammaProfileSurface {
fn delta_gamma_surface(
&self,
price_range: (Positive, Positive),
days_to_expiry: Vec<Positive>,
price_steps: usize,
) -> Result<Surface, SurfaceError>;
}
#[cfg(test)]
mod tests_delta_gamma_profile {
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 TestDeltaGammaProfile {
underlying_price: Positive,
}
impl DeltaGammaProfileCurve for TestDeltaGammaProfile {
fn delta_gamma_curve(&self) -> Result<Curve, CurveError> {
let mut points = BTreeSet::new();
let spot = self.underlying_price.to_dec();
let strikes = [
dec!(380.0),
dec!(400.0),
dec!(420.0),
dec!(440.0),
dec!(450.0),
dec!(460.0),
dec!(480.0),
dec!(500.0),
dec!(520.0),
];
for strike in strikes {
let moneyness = spot / strike;
let delta = if moneyness > Decimal::ONE {
dec!(0.5) + (moneyness - Decimal::ONE) * dec!(2.0)
} else {
dec!(0.5) - (Decimal::ONE - moneyness) * dec!(2.0)
};
let delta = delta.max(dec!(0.0)).min(dec!(1.0));
let gamma = dec!(0.05) * (-((strike - spot) / dec!(50.0)).powi(2)).exp();
let dollar_delta = delta * spot;
let dollar_gamma = gamma * spot * spot / dec!(100.0);
let combined = dollar_delta + dollar_gamma;
points.insert(Point2D::new(strike, combined));
}
Ok(Curve::new(points))
}
}
impl DeltaGammaProfileSurface for TestDeltaGammaProfile {
fn delta_gamma_surface(
&self,
price_range: (Positive, Positive),
days_to_expiry: Vec<Positive>,
price_steps: usize,
) -> Result<Surface, SurfaceError> {
let mut points = BTreeSet::new();
let price_step = if price_steps > 0 {
(price_range.1 - price_range.0).to_dec() / Decimal::from(price_steps)
} else {
Decimal::ZERO
};
for days in &days_to_expiry {
let time_factor = (days.to_dec() / dec!(365.0)).sqrt().unwrap_or(Decimal::ONE);
for p in 0..=price_steps {
let price = price_range.0.to_dec() + price_step * Decimal::from(p);
let atm = self.underlying_price.to_dec();
let moneyness = price / atm;
let base_delta = if moneyness > Decimal::ONE {
dec!(0.5) + (moneyness - Decimal::ONE) * dec!(2.0)
} else {
dec!(0.5) - (Decimal::ONE - moneyness) * dec!(2.0)
};
let delta = base_delta * time_factor
+ (Decimal::ONE - time_factor)
* if base_delta > dec!(0.5) {
Decimal::ONE
} else {
Decimal::ZERO
};
points.insert(Point3D::new(price, days.to_dec(), delta));
}
}
Ok(Surface::new(points))
}
}
#[test]
fn test_delta_gamma_curve_creation() {
let profile = TestDeltaGammaProfile {
underlying_price: pos_or_panic!(450.0),
};
let curve = profile.delta_gamma_curve();
assert!(curve.is_ok());
let curve = curve.unwrap();
assert_eq!(curve.points.len(), 9);
}
#[test]
fn test_delta_gamma_curve_monotonic_delta() {
let profile = TestDeltaGammaProfile {
underlying_price: pos_or_panic!(450.0),
};
let curve = profile.delta_gamma_curve().unwrap();
let points: Vec<&Point2D> = curve.points.iter().collect();
for i in 1..points.len() {
assert!(points[i].x > points[i - 1].x); }
}
#[test]
fn test_delta_gamma_surface_creation() {
let profile = TestDeltaGammaProfile {
underlying_price: pos_or_panic!(450.0),
};
let price_range = (pos_or_panic!(400.0), pos_or_panic!(500.0));
let days = vec![pos_or_panic!(7.0), pos_or_panic!(14.0), pos_or_panic!(30.0)];
let surface = profile.delta_gamma_surface(price_range, days, 10);
assert!(surface.is_ok());
let surface = surface.unwrap();
assert_eq!(surface.points.len(), 33);
}
#[test]
fn test_delta_gamma_surface_time_decay() {
let profile = TestDeltaGammaProfile {
underlying_price: pos_or_panic!(450.0),
};
let price_range = (pos_or_panic!(480.0), pos_or_panic!(480.0)); let days = vec![pos_or_panic!(30.0), pos_or_panic!(7.0), Positive::ONE];
let surface = profile.delta_gamma_surface(price_range, days, 0).unwrap();
let points: Vec<&Point3D> = surface.points.iter().collect();
let delta_30d = points.iter().find(|p| p.y == dec!(30.0)).map(|p| p.z);
let delta_1d = points.iter().find(|p| p.y == dec!(1.0)).map(|p| p.z);
if let (Some(d30), Some(d1)) = (delta_30d, delta_1d) {
assert!(d1 >= d30);
}
}
#[test]
fn test_delta_gamma_surface_empty_days() {
let profile = TestDeltaGammaProfile {
underlying_price: pos_or_panic!(450.0),
};
let price_range = (pos_or_panic!(400.0), pos_or_panic!(500.0));
let days: Vec<Positive> = vec![];
let surface = profile.delta_gamma_surface(price_range, days, 10).unwrap();
assert!(surface.points.is_empty());
}
}