use core::fmt;
use core::ops::{Add, Div, Mul, Neg, Sub};
use crate::errors::{IvError, PricingError};
use crate::iv::IvSolver;
use crate::models::bachelier::BachelierParams;
use crate::models::black76::Black76Params;
use crate::models::displaced::DisplacedParams;
pub trait Float:
Copy
+ PartialOrd
+ fmt::Debug
+ fmt::Display
+ Add<Output = Self>
+ Sub<Output = Self>
+ Mul<Output = Self>
+ Div<Output = Self>
+ Neg<Output = Self>
{
fn zero() -> Self;
fn one() -> Self;
#[must_use]
fn ln(self) -> Self;
#[must_use]
fn exp(self) -> Self;
#[must_use]
fn sqrt(self) -> Self;
#[must_use]
fn abs(self) -> Self;
#[must_use]
fn mul_add(self, a: Self, b: Self) -> Self;
fn from_f64(val: f64) -> Self;
fn to_f64(self) -> f64;
fn pi() -> Self;
fn is_nan(self) -> bool;
fn is_infinite(self) -> bool;
}
impl Float for f64 {
#[inline]
fn zero() -> Self {
0.0_f64
}
#[inline]
fn one() -> Self {
1.0_f64
}
#[inline]
fn ln(self) -> Self {
f64::ln(self)
}
#[inline]
fn exp(self) -> Self {
f64::exp(self)
}
#[inline]
fn sqrt(self) -> Self {
f64::sqrt(self)
}
#[inline]
fn abs(self) -> Self {
f64::abs(self)
}
#[inline]
fn mul_add(self, a: Self, b: Self) -> Self {
f64::mul_add(self, a, b)
}
#[inline]
fn from_f64(val: f64) -> Self {
val
}
#[inline]
fn to_f64(self) -> f64 {
self
}
#[inline]
fn pi() -> Self {
core::f64::consts::PI
}
#[inline]
fn is_nan(self) -> bool {
f64::is_nan(self)
}
#[inline]
fn is_infinite(self) -> bool {
f64::is_infinite(self)
}
}
impl Float for f32 {
#[inline]
fn zero() -> Self {
0.0_f32
}
#[inline]
fn one() -> Self {
1.0_f32
}
#[inline]
fn ln(self) -> Self {
f32::ln(self)
}
#[inline]
fn exp(self) -> Self {
f32::exp(self)
}
#[inline]
fn sqrt(self) -> Self {
f32::sqrt(self)
}
#[inline]
fn abs(self) -> Self {
f32::abs(self)
}
#[inline]
fn mul_add(self, a: Self, b: Self) -> Self {
f32::mul_add(self, a, b)
}
#[inline]
#[allow(clippy::cast_possible_truncation)] fn from_f64(val: f64) -> Self {
val as f32
}
#[inline]
fn to_f64(self) -> f64 {
f64::from(self)
}
#[inline]
fn pi() -> Self {
core::f32::consts::PI
}
#[inline]
fn is_nan(self) -> bool {
f32::is_nan(self)
}
#[inline]
fn is_infinite(self) -> bool {
f32::is_infinite(self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OptionType {
Call,
Put,
}
#[derive(Debug, Clone, Copy)]
pub struct OptionParams<F: Float> {
pub option_type: OptionType,
pub spot: F,
pub strike: F,
pub rate: F,
pub div_yield: F,
pub vol: F,
pub time: F,
}
#[derive(Debug, Clone, Copy)]
pub struct Greeks<F: Float> {
pub delta: F,
pub gamma: F,
pub theta: F,
pub vega: F,
pub rho: F,
pub epsilon: F,
pub lambda: F,
pub vanna: F,
pub charm: F,
pub veta: F,
pub vomma: F,
pub speed: F,
pub zomma: F,
pub color: F,
pub ultima: F,
pub dual_delta: F,
pub dual_gamma: F,
}
pub trait Pricing {
fn price(&self) -> Result<f64, PricingError>;
}
pub trait GreeksCalc {
fn greeks(&self) -> Result<Greeks<f64>, PricingError>;
}
pub trait ImpliedVol {
fn implied_vol(&self, market_price: f64, solver: IvSolver) -> Result<f64, IvError>;
}
impl Pricing for OptionParams<f64> {
#[inline]
fn price(&self) -> Result<f64, PricingError> {
crate::models::black_scholes::price(self)
}
}
impl GreeksCalc for OptionParams<f64> {
#[inline]
fn greeks(&self) -> Result<Greeks<f64>, PricingError> {
crate::greeks::compute_greeks(self)
}
}
impl ImpliedVol for OptionParams<f64> {
#[inline]
fn implied_vol(&self, market_price: f64, solver: IvSolver) -> Result<f64, IvError> {
crate::iv::implied_vol(self, market_price, solver)
}
}
impl Pricing for Black76Params {
#[inline]
fn price(&self) -> Result<f64, PricingError> {
crate::models::black76::price(self)
}
}
impl Pricing for BachelierParams {
#[inline]
fn price(&self) -> Result<f64, PricingError> {
crate::models::bachelier::price(self)
}
}
impl Pricing for DisplacedParams {
#[inline]
fn price(&self) -> Result<f64, PricingError> {
crate::models::displaced::price(self)
}
}
#[derive(Debug, Clone, Copy)]
pub enum Model {
BlackScholes(OptionParams<f64>),
Black76(Black76Params),
Bachelier(BachelierParams),
Displaced(DisplacedParams),
}
impl Pricing for Model {
#[inline]
fn price(&self) -> Result<f64, PricingError> {
match self {
Self::BlackScholes(p) => p.price(),
Self::Black76(p) => p.price(),
Self::Bachelier(p) => p.price(),
Self::Displaced(p) => p.price(),
}
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn test_option_type_clone_copy() {
let call = OptionType::Call;
let call2 = call;
assert_eq!(call, call2);
let put = OptionType::Put;
assert_ne!(call, put);
}
#[test]
fn test_option_type_debug() {
let call = OptionType::Call;
let debug_str = format!("{call:?}");
assert_eq!(debug_str, "Call");
}
#[test]
fn test_option_params_f64_construction() {
let params = OptionParams {
option_type: OptionType::Call,
spot: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
div_yield: 0.02_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
assert_eq!(params.spot.to_f64(), 100.0_f64);
assert_eq!(params.vol.to_f64(), 0.20_f64);
}
#[test]
fn test_option_params_f32_construction() {
let params = OptionParams {
option_type: OptionType::Put,
spot: 100.0_f32,
strike: 110.0_f32,
rate: 0.05_f32,
div_yield: 0.02_f32,
vol: 0.20_f32,
time: 1.0_f32,
};
assert!((params.spot.to_f64() - 100.0_f64).abs() < 1e-5_f64);
}
#[test]
fn test_option_params_copy() {
let p1 = OptionParams {
option_type: OptionType::Call,
spot: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
div_yield: 0.02_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let p2 = p1;
assert_eq!(p1.spot.to_f64(), p2.spot.to_f64());
}
#[test]
fn test_greeks_f64_construction() {
let g = Greeks {
delta: 0.5987_f64,
gamma: 0.0185_f64,
theta: -0.0152_f64,
vega: 0.3702_f64,
rho: 0.4174_f64,
epsilon: -0.5702_f64,
lambda: 6.47_f64,
vanna: -0.1314_f64,
charm: -0.0265_f64,
veta: 0.0_f64,
vomma: 0.1499_f64,
speed: -0.0006_f64,
zomma: -0.0009_f64,
color: 0.0_f64,
ultima: 0.0_f64,
dual_delta: -0.4741_f64,
dual_gamma: 0.0_f64,
};
assert!(g.delta > 0.0_f64);
assert!(g.gamma > 0.0_f64);
assert!(g.theta < 0.0_f64);
}
#[test]
fn test_greeks_copy() {
let g1 = Greeks {
delta: 0.5_f64,
gamma: 0.01_f64,
theta: -0.01_f64,
vega: 0.3_f64,
rho: 0.4_f64,
epsilon: -0.5_f64,
lambda: 6.0_f64,
vanna: -0.1_f64,
charm: -0.02_f64,
veta: 0.0_f64,
vomma: 0.1_f64,
speed: -0.0006_f64,
zomma: -0.0009_f64,
color: 0.0_f64,
ultima: 0.0_f64,
dual_delta: -0.4_f64,
dual_gamma: 0.0_f64,
};
let g2 = g1;
assert_eq!(g1.delta.to_f64(), g2.delta.to_f64());
}
#[test]
fn test_float_f64_zero_one() {
assert_eq!(f64::zero(), 0.0_f64);
assert_eq!(f64::one(), 1.0_f64);
}
#[test]
fn test_float_f64_ln_exp() {
let val = 2.0_f64;
let result = val.ln().exp();
assert!((result - 2.0_f64).abs() < 1e-15_f64);
}
#[test]
fn test_float_f64_sqrt() {
let val = 4.0_f64;
assert!((val.sqrt() - 2.0_f64).abs() < 1e-15_f64);
}
#[test]
fn test_float_f64_abs() {
assert_eq!((-3.0_f64).abs(), 3.0_f64);
assert_eq!(3.0_f64.abs(), 3.0_f64);
}
#[test]
fn test_float_f64_mul_add() {
let result = 2.0_f64.mul_add(3.0_f64, 4.0_f64);
assert!((result - 10.0_f64).abs() < 1e-15_f64);
}
#[test]
fn test_float_f64_from_f64() {
let val = f64::from_f64(3.25_f64);
assert!((val - 3.25_f64).abs() < 1e-15_f64);
}
#[test]
fn test_float_f32_roundtrip() {
let val = f32::from_f64(3.25_f64);
let back = val.to_f64();
assert!((back - 3.25_f64).abs() < 1e-5_f64);
}
#[test]
fn test_float_f64_pi() {
assert!((f64::pi() - core::f64::consts::PI).abs() < 1e-15_f64);
}
#[test]
fn test_float_f64_is_nan() {
assert!(f64::NAN.is_nan());
assert!(!1.0_f64.is_nan());
}
#[test]
fn test_float_f64_is_infinite() {
assert!(f64::INFINITY.is_infinite());
assert!(!1.0_f64.is_infinite());
}
#[test]
fn test_float_f32_basic_ops() {
let a = 2.0_f32;
let b = 3.0_f32;
assert!((a + b - 5.0_f32).abs() < 1e-6_f32);
assert!((a * b - 6.0_f32).abs() < 1e-6_f32);
assert!((b - a - 1.0_f32).abs() < 1e-6_f32);
assert!((b / a - 1.5_f32).abs() < 1e-6_f32);
assert!((-a + 2.0_f32).abs() < 1e-6_f32);
}
#[test]
fn test_pricing_trait_bs_call() {
let params = OptionParams {
option_type: OptionType::Call,
spot: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
div_yield: 0.02_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let p = params.price().unwrap();
assert!(
(p - 9.2270_f64).abs() < 1e-4_f64,
"BS call via trait: got {p}"
);
}
#[test]
fn test_pricing_trait_black76() {
let params = Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let p = params.price().unwrap();
assert!(p > 0.0_f64, "Black76 via trait: got {p}");
}
#[test]
fn test_pricing_trait_bachelier() {
let params = BachelierParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
normal_vol: 5.0_f64,
time: 1.0_f64,
};
let p = params.price().unwrap();
assert!(p > 0.0_f64, "Bachelier via trait: got {p}");
}
#[test]
fn test_pricing_trait_displaced() {
let params = DisplacedParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
displacement: 50.0_f64,
};
let p = params.price().unwrap();
assert!(p > 0.0_f64, "Displaced via trait: got {p}");
}
#[test]
fn test_greeks_calc_trait_call() {
let params = OptionParams {
option_type: OptionType::Call,
spot: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
div_yield: 0.02_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let g = params.greeks().unwrap();
assert!(g.delta > 0.0_f64 && g.delta < 1.0_f64);
assert!(g.gamma > 0.0_f64);
assert!(g.vega > 0.0_f64);
}
#[test]
fn test_implied_vol_trait_roundtrip() {
let params = OptionParams {
option_type: OptionType::Call,
spot: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
div_yield: 0.02_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let market_price = params.price().unwrap();
let iv_params = OptionParams {
vol: 0.0_f64,
..params
};
let iv = iv_params.implied_vol(market_price, IvSolver::Auto).unwrap();
assert!(
(iv - 0.20_f64).abs() < 1e-6_f64,
"IV roundtrip: expected ~0.20, got {iv}"
);
}
#[test]
fn test_model_enum_bs() {
let m = Model::BlackScholes(OptionParams {
option_type: OptionType::Call,
spot: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
div_yield: 0.02_f64,
vol: 0.20_f64,
time: 1.0_f64,
});
let p = m.price().unwrap();
assert!((p - 9.2270_f64).abs() < 1e-4_f64, "Model::BS: got {p}");
}
#[test]
fn test_model_enum_black76() {
let m = Model::Black76(Black76Params {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
});
let p = m.price().unwrap();
assert!(p > 0.0_f64, "Model::Black76: got {p}");
}
#[test]
fn test_model_enum_bachelier() {
let m = Model::Bachelier(BachelierParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
normal_vol: 5.0_f64,
time: 1.0_f64,
});
let p = m.price().unwrap();
assert!(p > 0.0_f64, "Model::Bachelier: got {p}");
}
#[test]
fn test_model_enum_displaced() {
let m = Model::Displaced(DisplacedParams {
option_type: OptionType::Call,
forward: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
vol: 0.20_f64,
time: 1.0_f64,
displacement: 50.0_f64,
});
let p = m.price().unwrap();
assert!(p > 0.0_f64, "Model::Displaced: got {p}");
}
#[test]
fn test_model_enum_matches_direct_call() {
let params = OptionParams {
option_type: OptionType::Call,
spot: 100.0_f64,
strike: 100.0_f64,
rate: 0.05_f64,
div_yield: 0.02_f64,
vol: 0.20_f64,
time: 1.0_f64,
};
let direct = params.price().unwrap();
let via_model = Model::BlackScholes(params).price().unwrap();
assert!(
(direct - via_model).abs() < 1e-15_f64,
"Model dispatch must match direct call"
);
}
}