pub mod greeks;
pub mod surface;
pub use greeks::{
GreekError, Greeks, OptionParams, OptionType, bsm_greeks, bsm_price, implied_volatility,
};
pub use surface::{VolPoint, VolSmile, VolSurface};
use crate::error::FinError;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
#[inline]
fn phi(x: f64) -> f64 {
crate::normal::pdf(x)
}
#[inline]
fn big_phi(x: f64) -> f64 {
crate::normal::cdf(x)
}
fn to_f64(d: Decimal) -> Result<f64, FinError> {
d.to_f64().ok_or(FinError::ArithmeticOverflow)
}
fn from_f64(f: f64) -> Result<Decimal, FinError> {
if !f.is_finite() {
return Err(FinError::ArithmeticOverflow);
}
Decimal::try_from(f).map_err(|_| FinError::ArithmeticOverflow)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum OptionKind {
Call,
Put,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OptionSpec {
pub kind: OptionKind,
pub spot: Decimal,
pub strike: Decimal,
pub time_to_expiry: Decimal,
pub risk_free_rate: Decimal,
pub volatility: Decimal,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct OptionGreeks {
pub price: Decimal,
pub delta: Decimal,
pub gamma: Decimal,
pub theta: Decimal,
pub vega: Decimal,
pub rho: Decimal,
}
pub struct BlackScholes;
impl BlackScholes {
pub fn price(spec: &OptionSpec) -> Result<OptionGreeks, FinError> {
Self::validate(spec)?;
let s = to_f64(spec.spot)?;
let k = to_f64(spec.strike)?;
let t = to_f64(spec.time_to_expiry)?;
let r = to_f64(spec.risk_free_rate)?;
let v = to_f64(spec.volatility)?;
let sqrt_t = t.sqrt();
let d1 = ((s / k).ln() + (r + 0.5 * v * v) * t) / (v * sqrt_t);
let d2 = d1 - v * sqrt_t;
let (price_f, delta_f, rho_f) = match spec.kind {
OptionKind::Call => {
let price = s * big_phi(d1) - k * (-r * t).exp() * big_phi(d2);
let delta = big_phi(d1);
let rho = k * t * (-r * t).exp() * big_phi(d2) * 0.01;
(price, delta, rho)
}
OptionKind::Put => {
let price = k * (-r * t).exp() * big_phi(-d2) - s * big_phi(-d1);
let delta = big_phi(d1) - 1.0;
let rho = -k * t * (-r * t).exp() * big_phi(-d2) * 0.01;
(price, delta, rho)
}
};
let gamma_f = phi(d1) / (s * v * sqrt_t);
let theta_f = match spec.kind {
OptionKind::Call => {
(-s * phi(d1) * v / (2.0 * sqrt_t)
- r * k * (-r * t).exp() * big_phi(d2))
/ 365.0
}
OptionKind::Put => {
(-s * phi(d1) * v / (2.0 * sqrt_t)
+ r * k * (-r * t).exp() * big_phi(-d2))
/ 365.0
}
};
let vega_f = s * sqrt_t * phi(d1) * 0.01;
Ok(OptionGreeks {
price: from_f64(price_f)?,
delta: from_f64(delta_f)?,
gamma: from_f64(gamma_f)?,
theta: from_f64(theta_f)?,
vega: from_f64(vega_f)?,
rho: from_f64(rho_f)?,
})
}
pub fn implied_volatility(
market_price: Decimal,
spot: Decimal,
strike: Decimal,
time_to_expiry: Decimal,
risk_free_rate: Decimal,
kind: OptionKind,
max_iter: usize,
tolerance: Decimal,
) -> Result<Decimal, FinError> {
if market_price <= Decimal::ZERO {
return Err(FinError::InvalidInput(
"Market price must be positive for IV solve".to_owned(),
));
}
let tol_f = to_f64(tolerance)?;
let target = to_f64(market_price)?;
let s_f = to_f64(spot)?;
let k_f = to_f64(strike)?;
let t_f = to_f64(time_to_expiry)?;
let mut sigma = (2.0 * std::f64::consts::PI / t_f).sqrt() * (target / s_f);
sigma = sigma.clamp(1e-6, 10.0);
for _ in 0..max_iter {
let vol_dec = from_f64(sigma)?;
let spec = OptionSpec {
kind,
spot,
strike,
time_to_expiry,
risk_free_rate,
volatility: vol_dec,
};
let greeks = Self::price(&spec)?;
let price_f = to_f64(greeks.price)?;
let vega_f = to_f64(greeks.vega)? * 100.0;
let diff = price_f - target;
if diff.abs() < tol_f {
return from_f64(sigma);
}
if vega_f.abs() < 1e-12 {
return Err(FinError::InvalidInput(
"Implied volatility solver: vega near zero, cannot converge".to_owned(),
));
}
sigma -= diff / vega_f;
sigma = sigma.clamp(1e-6, 10.0);
let _ = (s_f, k_f); }
Err(FinError::InvalidInput(format!(
"Implied volatility solver did not converge in {max_iter} iterations"
)))
}
fn validate(spec: &OptionSpec) -> Result<(), FinError> {
if spec.spot <= Decimal::ZERO {
return Err(FinError::InvalidPrice(spec.spot));
}
if spec.strike <= Decimal::ZERO {
return Err(FinError::InvalidPrice(spec.strike));
}
if spec.time_to_expiry <= Decimal::ZERO {
return Err(FinError::InvalidInput(
"time_to_expiry must be positive".to_owned(),
));
}
if spec.volatility <= Decimal::ZERO {
return Err(FinError::InvalidInput(
"volatility must be positive".to_owned(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn atm_call() -> OptionSpec {
OptionSpec {
kind: OptionKind::Call,
spot: dec!(100),
strike: dec!(100),
time_to_expiry: dec!(1),
risk_free_rate: dec!(0.05),
volatility: dec!(0.2),
}
}
#[test]
fn test_call_price_positive() {
let g = BlackScholes::price(&atm_call()).unwrap();
assert!(g.price > Decimal::ZERO);
}
#[test]
fn test_put_call_parity() {
let spec = atm_call();
let call = BlackScholes::price(&spec).unwrap();
let put_spec = OptionSpec { kind: OptionKind::Put, ..spec };
let put = BlackScholes::price(&put_spec).unwrap();
let diff = (call.price - put.price).abs();
assert!(diff > dec!(4) && diff < dec!(6), "put-call parity failed: {diff}");
}
#[test]
fn test_delta_call_between_zero_and_one() {
let g = BlackScholes::price(&atm_call()).unwrap();
assert!(g.delta > Decimal::ZERO && g.delta < dec!(1));
}
#[test]
fn test_gamma_positive() {
let g = BlackScholes::price(&atm_call()).unwrap();
assert!(g.gamma > Decimal::ZERO);
}
#[test]
fn test_vega_positive() {
let g = BlackScholes::price(&atm_call()).unwrap();
assert!(g.vega > Decimal::ZERO);
}
#[test]
fn test_invalid_spot_errors() {
let mut spec = atm_call();
spec.spot = dec!(0);
assert!(matches!(BlackScholes::price(&spec), Err(FinError::InvalidPrice(_))));
}
#[test]
fn test_implied_volatility_roundtrip() {
let spec = atm_call();
let g = BlackScholes::price(&spec).unwrap();
let iv = BlackScholes::implied_volatility(
g.price,
spec.spot,
spec.strike,
spec.time_to_expiry,
spec.risk_free_rate,
spec.kind,
200,
dec!(0.0001),
)
.unwrap();
let diff = (iv - spec.volatility).abs();
assert!(diff < dec!(0.001), "IV roundtrip error too large: {diff}");
}
}