use crate::curves::Curve;
use crate::error::CurveError;
use crate::error::SurfaceError;
use crate::surfaces::Surface;
use positive::Positive;
use rust_decimal::Decimal;
pub trait PriceShockCurve {
fn price_shock_curve(&self, shock_pct: Decimal) -> Result<Curve, CurveError>;
}
pub trait PriceShockSurface {
fn price_shock_surface(
&self,
price_range: (Positive, Positive),
vol_range: (Positive, Positive),
price_steps: usize,
vol_steps: usize,
) -> Result<Surface, SurfaceError>;
}
#[cfg(test)]
mod tests_price_shock {
use super::*;
use crate::curves::Point2D;
use crate::surfaces::Point3D;
use positive::pos_or_panic;
use rust_decimal::MathematicalOps;
use rust_decimal_macros::dec;
use std::collections::BTreeSet;
struct TestPriceShock {
underlying_price: Positive,
}
impl PriceShockCurve for TestPriceShock {
fn price_shock_curve(&self, shock_pct: Decimal) -> Result<Curve, CurveError> {
let mut points = BTreeSet::new();
let spot = self.underlying_price.to_dec();
let price_move = spot * shock_pct;
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) / spot;
let delta = dec!(0.5) + moneyness * dec!(2.0);
let delta = delta.max(dec!(0.0)).min(dec!(1.0));
let gamma = dec!(0.02) * (-((strike - spot) / dec!(30.0)).powi(2)).exp();
let pnl = delta * price_move + dec!(0.5) * gamma * price_move * price_move;
points.insert(Point2D::new(strike, pnl));
}
Ok(Curve::new(points))
}
}
impl PriceShockSurface for TestPriceShock {
fn price_shock_surface(
&self,
price_range: (Positive, Positive),
vol_range: (Positive, Positive),
price_steps: usize,
vol_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
};
let vol_step = if vol_steps > 0 {
(vol_range.1 - vol_range.0).to_dec() / Decimal::from(vol_steps)
} else {
Decimal::ZERO
};
let strike = self.underlying_price.to_dec();
for p in 0..=price_steps {
let price = price_range.0.to_dec() + price_step * Decimal::from(p);
for v in 0..=vol_steps {
let vol = vol_range.0.to_dec() + vol_step * Decimal::from(v);
let intrinsic = (price - strike).max(Decimal::ZERO);
let time_value = vol * price * dec!(0.1);
let option_value = intrinsic + time_value;
points.insert(Point3D::new(price, vol, option_value));
}
}
Ok(Surface::new(points))
}
}
#[test]
fn test_price_shock_curve_creation() {
let ps = TestPriceShock {
underlying_price: pos_or_panic!(450.0),
};
let curve = ps.price_shock_curve(dec!(-0.10));
assert!(curve.is_ok());
let curve = curve.unwrap();
assert_eq!(curve.points.len(), 9);
}
#[test]
fn test_price_shock_curve_negative_shock() {
let ps = TestPriceShock {
underlying_price: pos_or_panic!(450.0),
};
let curve = ps.price_shock_curve(dec!(-0.10)).unwrap();
let points: Vec<&Point2D> = curve.points.iter().collect();
let itm_pnl = points.iter().find(|p| p.x == dec!(380.0)).map(|p| p.y);
let otm_pnl = points.iter().find(|p| p.x == dec!(520.0)).map(|p| p.y);
if let (Some(itm), Some(otm)) = (itm_pnl, otm_pnl) {
assert!(itm < otm);
}
}
#[test]
fn test_price_shock_curve_positive_shock() {
let ps = TestPriceShock {
underlying_price: pos_or_panic!(450.0),
};
let curve = ps.price_shock_curve(dec!(0.10)).unwrap();
let points: Vec<&Point2D> = curve.points.iter().collect();
let itm_pnl = points.iter().find(|p| p.x == dec!(380.0)).map(|p| p.y);
let otm_pnl = points.iter().find(|p| p.x == dec!(520.0)).map(|p| p.y);
if let (Some(itm), Some(otm)) = (itm_pnl, otm_pnl) {
assert!(itm > otm);
}
}
#[test]
fn test_price_shock_surface_creation() {
let ps = TestPriceShock {
underlying_price: pos_or_panic!(450.0),
};
let price_range = (pos_or_panic!(400.0), pos_or_panic!(500.0));
let vol_range = (pos_or_panic!(0.10), pos_or_panic!(0.40));
let surface = ps.price_shock_surface(price_range, vol_range, 10, 10);
assert!(surface.is_ok());
let surface = surface.unwrap();
assert_eq!(surface.points.len(), 121);
}
#[test]
fn test_price_shock_surface_price_effect() {
let ps = TestPriceShock {
underlying_price: pos_or_panic!(450.0),
};
let price_range = (pos_or_panic!(400.0), pos_or_panic!(500.0));
let vol_range = (pos_or_panic!(0.20), pos_or_panic!(0.20));
let surface = ps
.price_shock_surface(price_range, vol_range, 10, 0)
.unwrap();
let points: Vec<&Point3D> = surface.points.iter().collect();
for i in 1..points.len() {
assert!(points[i].z >= points[i - 1].z);
}
}
#[test]
fn test_price_shock_surface_vol_effect() {
let ps = TestPriceShock {
underlying_price: pos_or_panic!(450.0),
};
let price_range = (pos_or_panic!(450.0), pos_or_panic!(450.0)); let vol_range = (pos_or_panic!(0.10), pos_or_panic!(0.40));
let surface = ps
.price_shock_surface(price_range, vol_range, 0, 10)
.unwrap();
let points: Vec<&Point3D> = surface.points.iter().collect();
for i in 1..points.len() {
assert!(points[i].z >= points[i - 1].z);
}
}
}