use std::{
fmt::Display,
ops::{Add, Deref, Mul},
};
use implied_vol::{DefaultSpecialFn, ImpliedBlackVolatility, SpecialFn};
use nautilus_core::{UnixNanos, datetime::unix_nanos_to_iso8601, math::quadratic_interpolation};
use crate::{
data::{
HasTsInit,
black_scholes::{compute_greeks, compute_iv_and_greeks},
},
identifiers::InstrumentId,
};
const FRAC_SQRT_2_PI: f64 = f64::from_bits(0x3fd9884533d43651);
const THETA_DAILY_FACTOR: f64 = 1.0 / 365.25;
const VEGA_PERCENT_FACTOR: f64 = 0.01;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Default)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct OptionGreekValues {
pub delta: f64,
pub gamma: f64,
pub vega: f64,
pub theta: f64,
pub rho: f64,
}
impl Add for OptionGreekValues {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self {
delta: self.delta + rhs.delta,
gamma: self.gamma + rhs.gamma,
vega: self.vega + rhs.vega,
theta: self.theta + rhs.theta,
rho: self.rho + rhs.rho,
}
}
}
impl Mul<f64> for OptionGreekValues {
type Output = Self;
fn mul(self, scalar: f64) -> Self {
Self {
delta: self.delta * scalar,
gamma: self.gamma * scalar,
vega: self.vega * scalar,
theta: self.theta * scalar,
rho: self.rho * scalar,
}
}
}
impl Mul<OptionGreekValues> for f64 {
type Output = OptionGreekValues;
fn mul(self, greeks: OptionGreekValues) -> OptionGreekValues {
greeks * self
}
}
impl Display for OptionGreekValues {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"OptionGreekValues(delta={:.4}, gamma={:.4}, vega={:.4}, theta={:.4}, rho={:.4})",
self.delta, self.gamma, self.vega, self.theta, self.rho
)
}
}
pub trait HasGreeks {
fn greeks(&self) -> OptionGreekValues;
}
#[inline(always)]
fn norm_pdf(x: f64) -> f64 {
FRAC_SQRT_2_PI * (-0.5 * x * x).exp()
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct BlackScholesGreeksResult {
pub price: f64,
pub vol: f64,
pub delta: f64,
pub gamma: f64,
pub vega: f64,
pub theta: f64,
pub itm_prob: f64,
}
#[allow(clippy::too_many_arguments)]
pub fn black_scholes_greeks_exact(
s: f64,
r: f64,
b: f64,
vol: f64,
is_call: bool,
k: f64,
t: f64,
) -> BlackScholesGreeksResult {
let phi = if is_call { 1.0 } else { -1.0 };
let sqrt_t = t.sqrt();
let scaled_vol = vol * sqrt_t;
let d1 = ((s / k).ln() + (b + 0.5 * vol.powi(2)) * t) / scaled_vol;
let d2 = d1 - scaled_vol;
let cdf_phi_d1 = DefaultSpecialFn::norm_cdf(phi * d1);
let cdf_phi_d2 = DefaultSpecialFn::norm_cdf(phi * d2);
let pdf_d1 = norm_pdf(d1);
let df_b = ((b - r) * t).exp();
let df_r = (-r * t).exp();
let price = phi * (s * df_b * cdf_phi_d1 - k * df_r * cdf_phi_d2);
let delta = phi * df_b * cdf_phi_d1;
let gamma = (df_b * pdf_d1) / (s * scaled_vol);
let vega = s * df_b * sqrt_t * pdf_d1 * VEGA_PERCENT_FACTOR;
let theta_v = -(s * df_b * pdf_d1 * vol) / (2.0 * sqrt_t);
let theta_b = -phi * (b - r) * s * df_b * cdf_phi_d1;
let theta_r = -phi * r * k * df_r * cdf_phi_d2;
let theta = (theta_v + theta_b + theta_r) * THETA_DAILY_FACTOR;
BlackScholesGreeksResult {
price,
vol,
delta,
gamma,
vega,
theta,
itm_prob: cdf_phi_d2,
}
}
pub fn imply_vol(s: f64, r: f64, b: f64, is_call: bool, k: f64, t: f64, price: f64) -> f64 {
let forward = s * (b * t).exp();
let forward_price = price * (r * t).exp();
ImpliedBlackVolatility::builder()
.option_price(forward_price)
.forward(forward)
.strike(k)
.expiry(t)
.is_call(is_call)
.build_unchecked()
.calculate::<DefaultSpecialFn>()
.unwrap_or(0.0)
}
#[allow(clippy::too_many_arguments)]
pub fn black_scholes_greeks(
s: f64,
r: f64,
b: f64,
vol: f64,
is_call: bool,
k: f64,
t: f64,
) -> BlackScholesGreeksResult {
let greeks = compute_greeks::<f32>(
s as f32, k as f32, t as f32, r as f32, b as f32, vol as f32, is_call,
);
BlackScholesGreeksResult {
price: (greeks.price as f64),
vol,
delta: (greeks.delta as f64),
gamma: (greeks.gamma as f64),
vega: (greeks.vega as f64) * VEGA_PERCENT_FACTOR,
theta: (greeks.theta as f64) * THETA_DAILY_FACTOR,
itm_prob: greeks.itm_prob as f64,
}
}
#[allow(clippy::too_many_arguments)]
pub fn imply_vol_and_greeks(
s: f64,
r: f64,
b: f64,
is_call: bool,
k: f64,
t: f64,
price: f64,
) -> BlackScholesGreeksResult {
let vol = imply_vol(s, r, b, is_call, k, t, price);
let safe_vol = if vol < 1e-8 { 1e-8 } else { vol };
black_scholes_greeks(s, r, b, safe_vol, is_call, k, t)
}
#[allow(clippy::too_many_arguments)]
pub fn refine_vol_and_greeks(
s: f64,
r: f64,
b: f64,
is_call: bool,
k: f64,
t: f64,
target_price: f64,
initial_vol: f64,
) -> BlackScholesGreeksResult {
let greeks = compute_iv_and_greeks::<f32>(
target_price as f32,
s as f32,
k as f32,
t as f32,
r as f32,
b as f32,
is_call,
initial_vol as f32,
);
BlackScholesGreeksResult {
price: (greeks.price as f64),
vol: greeks.vol as f64,
delta: (greeks.delta as f64),
gamma: (greeks.gamma as f64),
vega: (greeks.vega as f64) * VEGA_PERCENT_FACTOR,
theta: (greeks.theta as f64) * THETA_DAILY_FACTOR,
itm_prob: greeks.itm_prob as f64,
}
}
#[derive(Debug, Clone)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct GreeksData {
pub ts_init: UnixNanos,
pub ts_event: UnixNanos,
pub instrument_id: InstrumentId,
pub is_call: bool,
pub strike: f64,
pub expiry: i32,
pub expiry_in_days: i32,
pub expiry_in_years: f64,
pub multiplier: f64,
pub quantity: f64,
pub underlying_price: f64,
pub interest_rate: f64,
pub cost_of_carry: f64,
pub vol: f64,
pub pnl: f64,
pub price: f64,
pub greeks: OptionGreekValues,
pub itm_prob: f64,
}
impl GreeksData {
#[allow(clippy::too_many_arguments)]
pub fn new(
ts_init: UnixNanos,
ts_event: UnixNanos,
instrument_id: InstrumentId,
is_call: bool,
strike: f64,
expiry: i32,
expiry_in_days: i32,
expiry_in_years: f64,
multiplier: f64,
quantity: f64,
underlying_price: f64,
interest_rate: f64,
cost_of_carry: f64,
vol: f64,
pnl: f64,
price: f64,
greeks: OptionGreekValues,
itm_prob: f64,
) -> Self {
Self {
ts_init,
ts_event,
instrument_id,
is_call,
strike,
expiry,
expiry_in_days,
expiry_in_years,
multiplier,
quantity,
underlying_price,
interest_rate,
cost_of_carry,
vol,
pnl,
price,
greeks,
itm_prob,
}
}
pub fn from_delta(
instrument_id: InstrumentId,
delta: f64,
multiplier: f64,
ts_event: UnixNanos,
) -> Self {
Self {
ts_init: ts_event,
ts_event,
instrument_id,
is_call: true,
strike: 0.0,
expiry: 0,
expiry_in_days: 0,
expiry_in_years: 0.0,
multiplier,
quantity: 1.0,
underlying_price: 0.0,
interest_rate: 0.0,
cost_of_carry: 0.0,
vol: 0.0,
pnl: 0.0,
price: 0.0,
greeks: OptionGreekValues {
delta,
..Default::default()
},
itm_prob: 0.0,
}
}
}
impl Deref for GreeksData {
type Target = OptionGreekValues;
fn deref(&self) -> &Self::Target {
&self.greeks
}
}
impl HasGreeks for GreeksData {
fn greeks(&self) -> OptionGreekValues {
self.greeks
}
}
impl Default for GreeksData {
fn default() -> Self {
Self {
ts_init: UnixNanos::default(),
ts_event: UnixNanos::default(),
instrument_id: InstrumentId::from("ES.GLBX"),
is_call: true,
strike: 0.0,
expiry: 0,
expiry_in_days: 0,
expiry_in_years: 0.0,
multiplier: 0.0,
quantity: 0.0,
underlying_price: 0.0,
interest_rate: 0.0,
cost_of_carry: 0.0,
vol: 0.0,
pnl: 0.0,
price: 0.0,
greeks: OptionGreekValues::default(),
itm_prob: 0.0,
}
}
}
impl Display for GreeksData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"GreeksData(instrument_id={}, expiry={}, itm_prob={:.2}%, vol={:.2}%, pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, quantity={}, ts_init={})",
self.instrument_id,
self.expiry,
self.itm_prob * 100.0,
self.vol * 100.0,
self.pnl,
self.price,
self.greeks.delta,
self.greeks.gamma,
self.greeks.vega,
self.greeks.theta,
self.quantity,
unix_nanos_to_iso8601(self.ts_init)
)
}
}
impl Mul<&GreeksData> for f64 {
type Output = GreeksData;
fn mul(self, g: &GreeksData) -> GreeksData {
GreeksData {
ts_init: g.ts_init,
ts_event: g.ts_event,
instrument_id: g.instrument_id,
is_call: g.is_call,
strike: g.strike,
expiry: g.expiry,
expiry_in_days: g.expiry_in_days,
expiry_in_years: g.expiry_in_years,
multiplier: g.multiplier,
quantity: g.quantity,
underlying_price: g.underlying_price,
interest_rate: g.interest_rate,
cost_of_carry: g.cost_of_carry,
vol: g.vol,
pnl: self * g.pnl,
price: self * g.price,
greeks: g.greeks * self,
itm_prob: g.itm_prob,
}
}
}
impl HasTsInit for GreeksData {
fn ts_init(&self) -> UnixNanos {
self.ts_init
}
}
#[derive(Debug, Clone)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
)]
pub struct PortfolioGreeks {
pub ts_init: UnixNanos,
pub ts_event: UnixNanos,
pub pnl: f64,
pub price: f64,
pub greeks: OptionGreekValues,
}
impl PortfolioGreeks {
#[allow(clippy::too_many_arguments)]
pub fn new(
ts_init: UnixNanos,
ts_event: UnixNanos,
pnl: f64,
price: f64,
delta: f64,
gamma: f64,
vega: f64,
theta: f64,
) -> Self {
Self {
ts_init,
ts_event,
pnl,
price,
greeks: OptionGreekValues {
delta,
gamma,
vega,
theta,
rho: 0.0,
},
}
}
}
impl Deref for PortfolioGreeks {
type Target = OptionGreekValues;
fn deref(&self) -> &Self::Target {
&self.greeks
}
}
impl Default for PortfolioGreeks {
fn default() -> Self {
Self {
ts_init: UnixNanos::default(),
ts_event: UnixNanos::default(),
pnl: 0.0,
price: 0.0,
greeks: OptionGreekValues::default(),
}
}
}
impl Display for PortfolioGreeks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"PortfolioGreeks(pnl={:.2}, price={:.2}, delta={:.2}, gamma={:.2}, vega={:.2}, theta={:.2}, ts_event={}, ts_init={})",
self.pnl,
self.price,
self.greeks.delta,
self.greeks.gamma,
self.greeks.vega,
self.greeks.theta,
unix_nanos_to_iso8601(self.ts_event),
unix_nanos_to_iso8601(self.ts_init)
)
}
}
impl Add for PortfolioGreeks {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
ts_init: self.ts_init,
ts_event: self.ts_event,
pnl: self.pnl + other.pnl,
price: self.price + other.price,
greeks: self.greeks + other.greeks,
}
}
}
impl From<GreeksData> for PortfolioGreeks {
fn from(g: GreeksData) -> Self {
Self {
ts_init: g.ts_init,
ts_event: g.ts_event,
pnl: g.pnl,
price: g.price,
greeks: g.greeks,
}
}
}
impl HasTsInit for PortfolioGreeks {
fn ts_init(&self) -> UnixNanos {
self.ts_init
}
}
impl HasGreeks for PortfolioGreeks {
fn greeks(&self) -> OptionGreekValues {
self.greeks
}
}
impl HasGreeks for BlackScholesGreeksResult {
fn greeks(&self) -> OptionGreekValues {
OptionGreekValues {
delta: self.delta,
gamma: self.gamma,
vega: self.vega,
theta: self.theta,
rho: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct YieldCurveData {
pub ts_init: UnixNanos,
pub ts_event: UnixNanos,
pub curve_name: String,
pub tenors: Vec<f64>,
pub interest_rates: Vec<f64>,
}
impl YieldCurveData {
pub fn new(
ts_init: UnixNanos,
ts_event: UnixNanos,
curve_name: String,
tenors: Vec<f64>,
interest_rates: Vec<f64>,
) -> Self {
Self {
ts_init,
ts_event,
curve_name,
tenors,
interest_rates,
}
}
pub fn get_rate(&self, expiry_in_years: f64) -> f64 {
if self.interest_rates.len() == 1 {
return self.interest_rates[0];
}
quadratic_interpolation(expiry_in_years, &self.tenors, &self.interest_rates)
}
}
impl Display for YieldCurveData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"InterestRateCurve(curve_name={}, ts_event={}, ts_init={})",
self.curve_name,
unix_nanos_to_iso8601(self.ts_event),
unix_nanos_to_iso8601(self.ts_init)
)
}
}
impl HasTsInit for YieldCurveData {
fn ts_init(&self) -> UnixNanos {
self.ts_init
}
}
impl Default for YieldCurveData {
fn default() -> Self {
Self {
ts_init: UnixNanos::default(),
ts_event: UnixNanos::default(),
curve_name: "USD".to_string(),
tenors: vec![0.5, 1.0, 1.5, 2.0, 2.5],
interest_rates: vec![0.04, 0.04, 0.04, 0.04, 0.04],
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::identifiers::InstrumentId;
fn create_test_greeks_data() -> GreeksData {
GreeksData::new(
UnixNanos::from(1_000_000_000),
UnixNanos::from(1_500_000_000),
InstrumentId::from("SPY240315C00500000.OPRA"),
true,
500.0,
20240315,
91, 0.25,
100.0,
1.0,
520.0,
0.05,
0.05,
0.2,
250.0,
25.5,
OptionGreekValues {
delta: 0.65,
gamma: 0.003,
vega: 15.2,
theta: -0.08,
rho: 0.0,
},
0.75,
)
}
fn create_test_portfolio_greeks() -> PortfolioGreeks {
PortfolioGreeks::new(
UnixNanos::from(1_000_000_000),
UnixNanos::from(1_500_000_000),
1500.0,
125.5,
2.15,
0.008,
42.7,
-2.3,
)
}
fn create_test_yield_curve() -> YieldCurveData {
YieldCurveData::new(
UnixNanos::from(1_000_000_000),
UnixNanos::from(1_500_000_000),
"USD".to_string(),
vec![0.25, 0.5, 1.0, 2.0, 5.0],
vec![0.025, 0.03, 0.035, 0.04, 0.045],
)
}
#[rstest]
fn test_black_scholes_greeks_result_creation() {
let result = BlackScholesGreeksResult {
price: 25.5,
vol: 0.2,
delta: 0.65,
gamma: 0.003,
vega: 15.2,
theta: -0.08,
itm_prob: 0.55,
};
assert_eq!(result.price, 25.5);
assert_eq!(result.delta, 0.65);
assert_eq!(result.gamma, 0.003);
assert_eq!(result.vega, 15.2);
assert_eq!(result.theta, -0.08);
assert_eq!(result.itm_prob, 0.55);
}
#[rstest]
fn test_black_scholes_greeks_result_clone_and_copy() {
let result1 = BlackScholesGreeksResult {
price: 25.5,
vol: 0.2,
delta: 0.65,
gamma: 0.003,
vega: 15.2,
theta: -0.08,
itm_prob: 0.55,
};
let result2 = result1;
let result3 = result1;
assert_eq!(result1, result2);
assert_eq!(result1, result3);
}
#[rstest]
fn test_black_scholes_greeks_result_debug() {
let result = BlackScholesGreeksResult {
price: 25.5,
vol: 0.2,
delta: 0.65,
gamma: 0.003,
vega: 15.2,
theta: -0.08,
itm_prob: 0.55,
};
let debug_str = format!("{result:?}");
assert!(debug_str.contains("BlackScholesGreeksResult"));
assert!(debug_str.contains("25.5"));
assert!(debug_str.contains("0.65"));
}
#[rstest]
fn test_imply_vol_and_greeks_result_creation() {
let result = BlackScholesGreeksResult {
price: 25.5,
vol: 0.2,
delta: 0.65,
gamma: 0.003,
vega: 15.2,
theta: -0.08,
itm_prob: 0.55,
};
assert_eq!(result.vol, 0.2);
assert_eq!(result.price, 25.5);
assert_eq!(result.delta, 0.65);
assert_eq!(result.gamma, 0.003);
assert_eq!(result.vega, 15.2);
assert_eq!(result.theta, -0.08);
}
#[rstest]
fn test_black_scholes_greeks_basic_call() {
let s = 100.0;
let r = 0.05;
let b = 0.05;
let vol = 0.2;
let is_call = true;
let k = 100.0;
let t = 1.0;
let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
assert!(greeks.price > 0.0);
assert!(greeks.delta > 0.0 && greeks.delta < 1.0);
assert!(greeks.gamma > 0.0);
assert!(greeks.vega > 0.0);
assert!(greeks.theta < 0.0); }
#[rstest]
fn test_black_scholes_greeks_basic_put() {
let s = 100.0;
let r = 0.05;
let b = 0.05;
let vol = 0.2;
let is_call = false;
let k = 100.0;
let t = 1.0;
let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
assert!(
greeks.price > 0.0,
"Put option price should be positive, was: {}",
greeks.price
);
assert!(greeks.delta < 0.0 && greeks.delta > -1.0);
assert!(greeks.gamma > 0.0);
assert!(greeks.vega > 0.0);
assert!(greeks.theta < 0.0); }
#[rstest]
fn test_black_scholes_greeks_deep_itm_call() {
let s = 150.0;
let r = 0.05;
let b = 0.05;
let vol = 0.2;
let is_call = true;
let k = 100.0;
let t = 1.0;
let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
assert!(greeks.delta > 0.9); assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); }
#[rstest]
fn test_black_scholes_greeks_deep_otm_call() {
let s = 50.0;
let r = 0.05;
let b = 0.05;
let vol = 0.2;
let is_call = true;
let k = 100.0;
let t = 1.0;
let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
assert!(greeks.delta < 0.1); assert!(greeks.gamma > 0.0 && greeks.gamma < 0.01); }
#[rstest]
fn test_black_scholes_greeks_zero_time() {
let s = 100.0;
let r = 0.05;
let b = 0.05;
let vol = 0.2;
let is_call = true;
let k = 100.0;
let t = 0.0001;
let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
assert!(greeks.price >= 0.0);
assert!(greeks.theta.is_finite());
}
#[rstest]
fn test_imply_vol_basic() {
let s = 100.0;
let r = 0.05;
let b = 0.05;
let vol = 0.2;
let is_call = true;
let k = 100.0;
let t = 1.0;
let theoretical_price = black_scholes_greeks(s, r, b, vol, is_call, k, t).price;
let implied_vol = imply_vol(s, r, b, is_call, k, t, theoretical_price);
let tolerance = 1e-4;
assert!(
(implied_vol - vol).abs() < tolerance,
"Implied vol difference exceeds tolerance: {implied_vol} vs {vol}"
);
}
#[rstest]
fn test_greeks_data_new() {
let greeks = create_test_greeks_data();
assert_eq!(greeks.ts_init, UnixNanos::from(1_000_000_000));
assert_eq!(greeks.ts_event, UnixNanos::from(1_500_000_000));
assert_eq!(
greeks.instrument_id,
InstrumentId::from("SPY240315C00500000.OPRA")
);
assert!(greeks.is_call);
assert_eq!(greeks.strike, 500.0);
assert_eq!(greeks.expiry, 20240315);
assert_eq!(greeks.expiry_in_years, 0.25);
assert_eq!(greeks.multiplier, 100.0);
assert_eq!(greeks.quantity, 1.0);
assert_eq!(greeks.underlying_price, 520.0);
assert_eq!(greeks.interest_rate, 0.05);
assert_eq!(greeks.cost_of_carry, 0.05);
assert_eq!(greeks.vol, 0.2);
assert_eq!(greeks.pnl, 250.0);
assert_eq!(greeks.price, 25.5);
assert_eq!(greeks.delta, 0.65);
assert_eq!(greeks.gamma, 0.003);
assert_eq!(greeks.vega, 15.2);
assert_eq!(greeks.theta, -0.08);
assert_eq!(greeks.itm_prob, 0.75);
}
#[rstest]
fn test_greeks_data_from_delta() {
let delta = 0.5;
let multiplier = 100.0;
let ts_event = UnixNanos::from(2_000_000_000);
let instrument_id = InstrumentId::from("AAPL240315C00180000.OPRA");
let greeks = GreeksData::from_delta(instrument_id, delta, multiplier, ts_event);
assert_eq!(greeks.ts_init, ts_event);
assert_eq!(greeks.ts_event, ts_event);
assert_eq!(greeks.instrument_id, instrument_id);
assert!(greeks.is_call);
assert_eq!(greeks.delta, delta);
assert_eq!(greeks.multiplier, multiplier);
assert_eq!(greeks.quantity, 1.0);
assert_eq!(greeks.strike, 0.0);
assert_eq!(greeks.expiry, 0);
assert_eq!(greeks.price, 0.0);
assert_eq!(greeks.gamma, 0.0);
assert_eq!(greeks.vega, 0.0);
assert_eq!(greeks.theta, 0.0);
}
#[rstest]
fn test_greeks_data_default() {
let greeks = GreeksData::default();
assert_eq!(greeks.ts_init, UnixNanos::default());
assert_eq!(greeks.ts_event, UnixNanos::default());
assert_eq!(greeks.instrument_id, InstrumentId::from("ES.GLBX"));
assert!(greeks.is_call);
assert_eq!(greeks.strike, 0.0);
assert_eq!(greeks.expiry, 0);
assert_eq!(greeks.multiplier, 0.0);
assert_eq!(greeks.quantity, 0.0);
assert_eq!(greeks.delta, 0.0);
assert_eq!(greeks.gamma, 0.0);
assert_eq!(greeks.vega, 0.0);
assert_eq!(greeks.theta, 0.0);
}
#[rstest]
fn test_greeks_data_display() {
let greeks = create_test_greeks_data();
let display_str = format!("{greeks}");
assert!(display_str.contains("GreeksData"));
assert!(display_str.contains("SPY240315C00500000.OPRA"));
assert!(display_str.contains("20240315"));
assert!(display_str.contains("75.00%")); assert!(display_str.contains("20.00%")); assert!(display_str.contains("250.00")); assert!(display_str.contains("25.50")); assert!(display_str.contains("0.65")); }
#[rstest]
fn test_greeks_data_multiplication() {
let greeks = create_test_greeks_data();
let quantity = 5.0;
let scaled_greeks = quantity * &greeks;
assert_eq!(scaled_greeks.ts_init, greeks.ts_init);
assert_eq!(scaled_greeks.ts_event, greeks.ts_event);
assert_eq!(scaled_greeks.instrument_id, greeks.instrument_id);
assert_eq!(scaled_greeks.is_call, greeks.is_call);
assert_eq!(scaled_greeks.strike, greeks.strike);
assert_eq!(scaled_greeks.expiry, greeks.expiry);
assert_eq!(scaled_greeks.multiplier, greeks.multiplier);
assert_eq!(scaled_greeks.quantity, greeks.quantity);
assert_eq!(scaled_greeks.vol, greeks.vol);
assert_eq!(scaled_greeks.itm_prob, greeks.itm_prob);
assert_eq!(scaled_greeks.pnl, quantity * greeks.pnl);
assert_eq!(scaled_greeks.price, quantity * greeks.price);
assert_eq!(scaled_greeks.delta, quantity * greeks.delta);
assert_eq!(scaled_greeks.gamma, quantity * greeks.gamma);
assert_eq!(scaled_greeks.vega, quantity * greeks.vega);
assert_eq!(scaled_greeks.theta, quantity * greeks.theta);
}
#[rstest]
fn test_greeks_data_has_ts_init() {
let greeks = create_test_greeks_data();
assert_eq!(greeks.ts_init(), UnixNanos::from(1_000_000_000));
}
#[rstest]
fn test_greeks_data_clone() {
let greeks1 = create_test_greeks_data();
let greeks2 = greeks1.clone();
assert_eq!(greeks1.ts_init, greeks2.ts_init);
assert_eq!(greeks1.instrument_id, greeks2.instrument_id);
assert_eq!(greeks1.delta, greeks2.delta);
assert_eq!(greeks1.gamma, greeks2.gamma);
}
#[rstest]
fn test_portfolio_greeks_new() {
let portfolio_greeks = create_test_portfolio_greeks();
assert_eq!(portfolio_greeks.ts_init, UnixNanos::from(1_000_000_000));
assert_eq!(portfolio_greeks.ts_event, UnixNanos::from(1_500_000_000));
assert_eq!(portfolio_greeks.pnl, 1500.0);
assert_eq!(portfolio_greeks.price, 125.5);
assert_eq!(portfolio_greeks.delta, 2.15);
assert_eq!(portfolio_greeks.gamma, 0.008);
assert_eq!(portfolio_greeks.vega, 42.7);
assert_eq!(portfolio_greeks.theta, -2.3);
}
#[rstest]
fn test_portfolio_greeks_default() {
let portfolio_greeks = PortfolioGreeks::default();
assert_eq!(portfolio_greeks.ts_init, UnixNanos::default());
assert_eq!(portfolio_greeks.ts_event, UnixNanos::default());
assert_eq!(portfolio_greeks.pnl, 0.0);
assert_eq!(portfolio_greeks.price, 0.0);
assert_eq!(portfolio_greeks.delta, 0.0);
assert_eq!(portfolio_greeks.gamma, 0.0);
assert_eq!(portfolio_greeks.vega, 0.0);
assert_eq!(portfolio_greeks.theta, 0.0);
}
#[rstest]
fn test_portfolio_greeks_display() {
let portfolio_greeks = create_test_portfolio_greeks();
let display_str = format!("{portfolio_greeks}");
assert!(display_str.contains("PortfolioGreeks"));
assert!(display_str.contains("1500.00")); assert!(display_str.contains("125.50")); assert!(display_str.contains("2.15")); assert!(display_str.contains("0.01")); assert!(display_str.contains("42.70")); assert!(display_str.contains("-2.30")); }
#[rstest]
fn test_portfolio_greeks_addition() {
let greeks1 = PortfolioGreeks::new(
UnixNanos::from(1_000_000_000),
UnixNanos::from(1_500_000_000),
100.0,
50.0,
1.0,
0.005,
20.0,
-1.0,
);
let greeks2 = PortfolioGreeks::new(
UnixNanos::from(2_000_000_000),
UnixNanos::from(2_500_000_000),
200.0,
75.0,
1.5,
0.003,
25.0,
-1.5,
);
let result = greeks1 + greeks2;
assert_eq!(result.ts_init, UnixNanos::from(1_000_000_000)); assert_eq!(result.ts_event, UnixNanos::from(1_500_000_000)); assert_eq!(result.pnl, 300.0);
assert_eq!(result.price, 125.0);
assert_eq!(result.delta, 2.5);
assert_eq!(result.gamma, 0.008);
assert_eq!(result.vega, 45.0);
assert_eq!(result.theta, -2.5);
}
#[rstest]
fn test_portfolio_greeks_from_greeks_data() {
let greeks_data = create_test_greeks_data();
let portfolio_greeks: PortfolioGreeks = greeks_data.clone().into();
assert_eq!(portfolio_greeks.ts_init, greeks_data.ts_init);
assert_eq!(portfolio_greeks.ts_event, greeks_data.ts_event);
assert_eq!(portfolio_greeks.pnl, greeks_data.pnl);
assert_eq!(portfolio_greeks.price, greeks_data.price);
assert_eq!(portfolio_greeks.delta, greeks_data.delta);
assert_eq!(portfolio_greeks.gamma, greeks_data.gamma);
assert_eq!(portfolio_greeks.vega, greeks_data.vega);
assert_eq!(portfolio_greeks.theta, greeks_data.theta);
}
#[rstest]
fn test_portfolio_greeks_has_ts_init() {
let portfolio_greeks = create_test_portfolio_greeks();
assert_eq!(portfolio_greeks.ts_init(), UnixNanos::from(1_000_000_000));
}
#[rstest]
fn test_yield_curve_data_new() {
let curve = create_test_yield_curve();
assert_eq!(curve.ts_init, UnixNanos::from(1_000_000_000));
assert_eq!(curve.ts_event, UnixNanos::from(1_500_000_000));
assert_eq!(curve.curve_name, "USD");
assert_eq!(curve.tenors, vec![0.25, 0.5, 1.0, 2.0, 5.0]);
assert_eq!(curve.interest_rates, vec![0.025, 0.03, 0.035, 0.04, 0.045]);
}
#[rstest]
fn test_yield_curve_data_default() {
let curve = YieldCurveData::default();
assert_eq!(curve.ts_init, UnixNanos::default());
assert_eq!(curve.ts_event, UnixNanos::default());
assert_eq!(curve.curve_name, "USD");
assert_eq!(curve.tenors, vec![0.5, 1.0, 1.5, 2.0, 2.5]);
assert_eq!(curve.interest_rates, vec![0.04, 0.04, 0.04, 0.04, 0.04]);
}
#[rstest]
fn test_yield_curve_data_get_rate_single_point() {
let curve = YieldCurveData::new(
UnixNanos::default(),
UnixNanos::default(),
"USD".to_string(),
vec![1.0],
vec![0.05],
);
assert_eq!(curve.get_rate(0.5), 0.05);
assert_eq!(curve.get_rate(1.0), 0.05);
assert_eq!(curve.get_rate(2.0), 0.05);
}
#[rstest]
fn test_yield_curve_data_get_rate_interpolation() {
let curve = create_test_yield_curve();
assert_eq!(curve.get_rate(0.25), 0.025);
assert_eq!(curve.get_rate(1.0), 0.035);
assert_eq!(curve.get_rate(5.0), 0.045);
let rate_0_75 = curve.get_rate(0.75);
assert!(rate_0_75 > 0.025 && rate_0_75 < 0.045);
}
#[rstest]
fn test_yield_curve_data_display() {
let curve = create_test_yield_curve();
let display_str = format!("{curve}");
assert!(display_str.contains("InterestRateCurve"));
assert!(display_str.contains("USD"));
}
#[rstest]
fn test_yield_curve_data_has_ts_init() {
let curve = create_test_yield_curve();
assert_eq!(curve.ts_init(), UnixNanos::from(1_000_000_000));
}
#[rstest]
fn test_yield_curve_data_clone() {
let curve1 = create_test_yield_curve();
let curve2 = curve1.clone();
assert_eq!(curve1.curve_name, curve2.curve_name);
assert_eq!(curve1.tenors, curve2.tenors);
assert_eq!(curve1.interest_rates, curve2.interest_rates);
}
#[rstest]
fn test_black_scholes_greeks_extreme_values() {
let s = 1000.0;
let r = 0.1;
let b = 0.1;
let vol = 0.5;
let is_call = true;
let k = 10.0; let t = 0.1;
let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
assert!(greeks.price.is_finite());
assert!(greeks.delta.is_finite());
assert!(greeks.gamma.is_finite());
assert!(greeks.vega.is_finite());
assert!(greeks.theta.is_finite());
assert!(greeks.price > 0.0);
assert!(greeks.delta > 0.99); }
#[rstest]
fn test_black_scholes_greeks_high_volatility() {
let s = 100.0;
let r = 0.05;
let b = 0.05;
let vol = 2.0; let is_call = true;
let k = 100.0;
let t = 1.0;
let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
assert!(greeks.price.is_finite());
assert!(greeks.delta.is_finite());
assert!(greeks.gamma.is_finite());
assert!(greeks.vega.is_finite());
assert!(greeks.theta.is_finite());
assert!(greeks.price > 0.0);
}
#[rstest]
fn test_greeks_data_put_option() {
let greeks = GreeksData::new(
UnixNanos::from(1_000_000_000),
UnixNanos::from(1_500_000_000),
InstrumentId::from("SPY240315P00480000.OPRA"),
false, 480.0,
20240315,
91, 0.25,
100.0,
1.0,
500.0,
0.05,
0.05,
0.25,
-150.0, 8.5,
OptionGreekValues {
delta: -0.35,
gamma: 0.002,
vega: 12.8,
theta: -0.06,
rho: 0.0,
},
0.25,
);
assert!(!greeks.is_call);
assert!(greeks.delta < 0.0);
assert_eq!(greeks.pnl, -150.0);
}
#[rstest]
fn test_greeks_accuracy_call() {
let s = 100.0;
let k = 100.1;
let t = 1.0;
let r = 0.01;
let b = 0.005;
let vol = 0.2;
let is_call = true;
let eps = 1e-3;
let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
let price0 = |s: f64| black_scholes_greeks_exact(s, r, b, vol, is_call, k, t).price;
let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
let vega_bnr = (black_scholes_greeks_exact(s, r, b, vol + eps, is_call, k, t).price
- black_scholes_greeks_exact(s, r, b, vol - eps, is_call, k, t).price)
/ (2.0 * eps)
/ 100.0;
let theta_bnr = (black_scholes_greeks_exact(s, r, b, vol, is_call, k, t - eps).price
- black_scholes_greeks_exact(s, r, b, vol, is_call, k, t + eps).price)
/ (2.0 * eps)
/ 365.25;
let tolerance = 5e-3;
assert!(
(greeks.delta - delta_bnr).abs() < tolerance,
"Delta difference exceeds tolerance: {} vs {}",
greeks.delta,
delta_bnr
);
let gamma_tolerance = 0.1;
assert!(
(greeks.gamma - gamma_bnr).abs() < gamma_tolerance,
"Gamma difference exceeds tolerance: {} vs {}",
greeks.gamma,
gamma_bnr
);
assert!(
(greeks.vega - vega_bnr).abs() < tolerance,
"Vega difference exceeds tolerance: {} vs {}",
greeks.vega,
vega_bnr
);
assert!(
(greeks.theta - theta_bnr).abs() < tolerance,
"Theta difference exceeds tolerance: {} vs {}",
greeks.theta,
theta_bnr
);
}
#[rstest]
fn test_greeks_accuracy_put() {
let s = 100.0;
let k = 100.1;
let t = 1.0;
let r = 0.01;
let b = 0.005;
let vol = 0.2;
let is_call = false;
let eps = 1e-3;
let greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
let price0 = |s: f64| black_scholes_greeks_exact(s, r, b, vol, is_call, k, t).price;
let delta_bnr = (price0(s + eps) - price0(s - eps)) / (2.0 * eps);
let gamma_bnr = (price0(s + eps) + price0(s - eps) - 2.0 * price0(s)) / (eps * eps);
let vega_bnr = (black_scholes_greeks_exact(s, r, b, vol + eps, is_call, k, t).price
- black_scholes_greeks_exact(s, r, b, vol - eps, is_call, k, t).price)
/ (2.0 * eps)
/ 100.0;
let theta_bnr = (black_scholes_greeks_exact(s, r, b, vol, is_call, k, t - eps).price
- black_scholes_greeks_exact(s, r, b, vol, is_call, k, t + eps).price)
/ (2.0 * eps)
/ 365.25;
let tolerance = 5e-3;
assert!(
(greeks.delta - delta_bnr).abs() < tolerance,
"Delta difference exceeds tolerance: {} vs {}",
greeks.delta,
delta_bnr
);
let gamma_tolerance = 0.1;
assert!(
(greeks.gamma - gamma_bnr).abs() < gamma_tolerance,
"Gamma difference exceeds tolerance: {} vs {}",
greeks.gamma,
gamma_bnr
);
assert!(
(greeks.vega - vega_bnr).abs() < tolerance,
"Vega difference exceeds tolerance: {} vs {}",
greeks.vega,
vega_bnr
);
assert!(
(greeks.theta - theta_bnr).abs() < tolerance,
"Theta difference exceeds tolerance: {} vs {}",
greeks.theta,
theta_bnr
);
}
#[rstest]
fn test_imply_vol_and_greeks_accuracy_call() {
let s = 100.0;
let k = 100.1;
let t = 1.0;
let r = 0.01;
let b = 0.005;
let vol = 0.2;
let is_call = true;
let base_greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
let price = base_greeks.price;
let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price);
let tolerance = 2e-4;
assert!(
(implied_result.vol - vol).abs() < tolerance,
"Vol difference exceeds tolerance: {} vs {}",
implied_result.vol,
vol
);
assert!(
(implied_result.price - base_greeks.price).abs() < tolerance,
"Price difference exceeds tolerance: {} vs {}",
implied_result.price,
base_greeks.price
);
assert!(
(implied_result.delta - base_greeks.delta).abs() < tolerance,
"Delta difference exceeds tolerance: {} vs {}",
implied_result.delta,
base_greeks.delta
);
assert!(
(implied_result.gamma - base_greeks.gamma).abs() < tolerance,
"Gamma difference exceeds tolerance: {} vs {}",
implied_result.gamma,
base_greeks.gamma
);
assert!(
(implied_result.vega - base_greeks.vega).abs() < tolerance,
"Vega difference exceeds tolerance: {} vs {}",
implied_result.vega,
base_greeks.vega
);
assert!(
(implied_result.theta - base_greeks.theta).abs() < tolerance,
"Theta difference exceeds tolerance: {} vs {}",
implied_result.theta,
base_greeks.theta
);
}
#[rstest]
fn test_black_scholes_greeks_target_price_refinement() {
let s = 100.0;
let r = 0.05;
let b = 0.05;
let initial_vol = 0.2;
let is_call = true;
let k = 100.0;
let t = 1.0;
let initial_greeks = black_scholes_greeks(s, r, b, initial_vol, is_call, k, t);
let target_price = initial_greeks.price;
let refined_vol = initial_vol * 1.1; let refined_greeks =
refine_vol_and_greeks(s, r, b, is_call, k, t, target_price, refined_vol);
let price_tolerance = (s * 5e-5).max(1e-4) * 2.0;
assert!(
(refined_greeks.price - target_price).abs() < price_tolerance,
"Refined price should match target: {} vs {}",
refined_greeks.price,
target_price
);
assert!(
refined_vol > refined_greeks.vol && refined_greeks.vol > initial_vol * 0.9,
"Refined vol should converge towards initial: {} (initial: {}, refined: {})",
refined_greeks.vol,
initial_vol,
refined_vol
);
}
#[rstest]
fn test_black_scholes_greeks_target_price_refinement_put() {
let s = 100.0;
let r = 0.05;
let b = 0.05;
let initial_vol = 0.25;
let is_call = false;
let k = 105.0;
let t = 0.5;
let initial_greeks = black_scholes_greeks(s, r, b, initial_vol, is_call, k, t);
let target_price = initial_greeks.price;
let refined_vol = initial_vol * 0.8; let refined_greeks =
refine_vol_and_greeks(s, r, b, is_call, k, t, target_price, refined_vol);
let price_tolerance = (s * 5e-5).max(1e-4) * 2.0;
assert!(
(refined_greeks.price - target_price).abs() < price_tolerance,
"Refined price should match target: {} vs {}",
refined_greeks.price,
target_price
);
assert!(
refined_vol < refined_greeks.vol && refined_greeks.vol < initial_vol * 1.1,
"Refined vol should converge towards initial: {} (initial: {}, refined: {})",
refined_greeks.vol,
initial_vol,
refined_vol
);
}
#[rstest]
fn test_imply_vol_and_greeks_accuracy_put() {
let s = 100.0;
let k = 100.1;
let t = 1.0;
let r = 0.01;
let b = 0.005;
let vol = 0.2;
let is_call = false;
let base_greeks = black_scholes_greeks(s, r, b, vol, is_call, k, t);
let price = base_greeks.price;
let implied_result = imply_vol_and_greeks(s, r, b, is_call, k, t, price);
let tolerance = 2e-4;
assert!(
(implied_result.vol - vol).abs() < tolerance,
"Vol difference exceeds tolerance: {} vs {}",
implied_result.vol,
vol
);
assert!(
(implied_result.price - base_greeks.price).abs() < tolerance,
"Price difference exceeds tolerance: {} vs {}",
implied_result.price,
base_greeks.price
);
assert!(
(implied_result.delta - base_greeks.delta).abs() < tolerance,
"Delta difference exceeds tolerance: {} vs {}",
implied_result.delta,
base_greeks.delta
);
assert!(
(implied_result.gamma - base_greeks.gamma).abs() < tolerance,
"Gamma difference exceeds tolerance: {} vs {}",
implied_result.gamma,
base_greeks.gamma
);
assert!(
(implied_result.vega - base_greeks.vega).abs() < tolerance,
"Vega difference exceeds tolerance: {} vs {}",
implied_result.vega,
base_greeks.vega
);
assert!(
(implied_result.theta - base_greeks.theta).abs() < tolerance,
"Theta difference exceeds tolerance: {} vs {}",
implied_result.theta,
base_greeks.theta
);
}
#[rstest]
fn test_black_scholes_greeks_vs_exact(
#[values(90.0, 100.0, 110.0)] spot: f64,
#[values(true, false)] is_call: bool,
#[values(0.15, 0.25, 0.5)] vol: f64,
#[values(0.01, 0.25, 2.0)] t: f64,
) {
let r = 0.05;
let b = 0.05;
let k = 100.0;
let greeks_fast = black_scholes_greeks(spot, r, b, vol, is_call, k, t);
let greeks_exact = black_scholes_greeks_exact(spot, r, b, vol, is_call, k, t);
let rel_tolerance = if t < 0.1 {
1e-4 } else {
8e-6 };
let abs_tolerance = 1e-10;
let check_7_sig_figs = |fast: f64, exact: f64, name: &str| {
let abs_diff = (fast - exact).abs();
let small_value_threshold = 1e-4;
let max_allowed = if exact.abs() < small_value_threshold {
if t < 0.1 {
1e-5 } else {
1e-6 }
} else {
exact.abs().max(abs_tolerance) * rel_tolerance
};
let rel_diff = if exact.abs() > abs_tolerance {
abs_diff / exact.abs()
} else {
0.0 };
assert!(
abs_diff < max_allowed,
"{name} mismatch for spot={spot}, is_call={is_call}, vol={vol}, t={t}: fast={fast:.10}, exact={exact:.10}, abs_diff={abs_diff:.2e}, rel_diff={rel_diff:.2e}, max_allowed={max_allowed:.2e}"
);
};
check_7_sig_figs(greeks_fast.price, greeks_exact.price, "Price");
check_7_sig_figs(greeks_fast.delta, greeks_exact.delta, "Delta");
check_7_sig_figs(greeks_fast.gamma, greeks_exact.gamma, "Gamma");
check_7_sig_figs(greeks_fast.vega, greeks_exact.vega, "Vega");
check_7_sig_figs(greeks_fast.theta, greeks_exact.theta, "Theta");
}
#[rstest]
fn test_refine_vol_and_greeks_vs_imply_vol_and_greeks(
#[values(90.0, 100.0, 110.0)] spot: f64,
#[values(true, false)] is_call: bool,
#[values(0.15, 0.25, 0.5)] target_vol: f64,
#[values(0.01, 0.25, 2.0)] t: f64,
) {
let r = 0.05;
let b = 0.05;
let k = 100.0;
let base_greeks = black_scholes_greeks(spot, r, b, target_vol, is_call, k, t);
let target_price = base_greeks.price;
let initial_guess = target_vol - 0.01;
let refined_result =
refine_vol_and_greeks(spot, r, b, is_call, k, t, target_price, initial_guess);
let implied_result = imply_vol_and_greeks(spot, r, b, is_call, k, t, target_price);
let moneyness = (spot - k) / k;
let is_deep_itm_otm = moneyness.abs() > 0.05;
let is_deep_edge_case = t < 0.1 && is_deep_itm_otm;
let vol_abs_tolerance = 1e-6;
let vol_rel_tolerance = if is_deep_edge_case {
2.0 } else if t < 0.1 {
0.10 } else if t > 1.5 {
if target_vol <= 0.15 {
0.05 } else {
0.01 }
} else {
if target_vol <= 0.15 {
0.05 } else {
0.001 }
};
let refined_vol_error = (refined_result.vol - target_vol).abs();
let implied_vol_error = (implied_result.vol - target_vol).abs();
let refined_vol_rel_error = refined_vol_error / target_vol.max(vol_abs_tolerance);
let implied_vol_rel_error = implied_vol_error / target_vol.max(vol_abs_tolerance);
assert!(
refined_vol_rel_error < vol_rel_tolerance,
"Refined vol mismatch for spot={}, is_call={}, target_vol={}, t={}: refined={:.10}, target={:.10}, abs_error={:.2e}, rel_error={:.2e}",
spot,
is_call,
target_vol,
t,
refined_result.vol,
target_vol,
refined_vol_error,
refined_vol_rel_error
);
let implied_vol_tolerance = if is_deep_edge_case {
2.0 } else if implied_result.vol < 1e-6 {
2.0 } else if t < 0.1 && (implied_result.vol - target_vol).abs() / target_vol.max(1e-6) > 0.5 {
2.0 } else {
vol_rel_tolerance
};
assert!(
implied_vol_rel_error < implied_vol_tolerance,
"Implied vol mismatch for spot={}, is_call={}, target_vol={}, t={}: implied={:.10}, target={:.10}, abs_error={:.2e}, rel_error={:.2e}",
spot,
is_call,
target_vol,
t,
implied_result.vol,
target_vol,
implied_vol_error,
implied_vol_rel_error
);
let greeks_abs_tolerance = 1e-10;
let moneyness = (spot - k) / k;
let is_deep_itm_otm = moneyness.abs() > 0.05;
let is_deep_edge_case = t < 0.1 && is_deep_itm_otm;
let greeks_rel_tolerance = if is_deep_edge_case {
1.0 } else if t < 0.1 {
if target_vol <= 0.15 {
0.10 } else {
0.05 }
} else if t > 1.5 {
if target_vol <= 0.15 {
0.08 } else {
0.01 }
} else {
if target_vol <= 0.15 {
0.05 } else {
2e-3 }
};
let imply_vol_failed = implied_result.vol < 1e-6
|| (t < 0.1 && (implied_result.vol - target_vol).abs() / target_vol.max(1e-6) > 0.5)
|| is_deep_edge_case;
let effective_greeks_tolerance = if imply_vol_failed || is_deep_edge_case {
1.0 } else {
greeks_rel_tolerance
};
let check_6_sig_figs = |refined: f64, implied: f64, name: &str, is_gamma: bool| {
if (imply_vol_failed || is_deep_edge_case)
&& (!implied.is_finite() || implied.abs() < 1e-4 || refined.abs() < 1e-4)
{
return; }
let abs_diff = (refined - implied).abs();
let small_value_threshold = if is_deep_edge_case { 1e-3 } else { 1e-6 };
let rel_diff =
if implied.abs() < small_value_threshold && refined.abs() < small_value_threshold {
0.0 } else {
abs_diff / implied.abs().max(greeks_abs_tolerance)
};
let gamma_multiplier = if (0.1..=1.5).contains(&t) {
if target_vol <= 0.15 { 5.0 } else { 3.0 }
} else {
if target_vol <= 0.15 { 10.0 } else { 5.0 }
};
let tolerance = if is_gamma {
effective_greeks_tolerance * gamma_multiplier
} else {
effective_greeks_tolerance
};
let max_allowed = if is_deep_edge_case && implied.abs() < 1e-3 {
2e-5 } else {
implied.abs().max(greeks_abs_tolerance) * tolerance
};
assert!(
abs_diff < max_allowed,
"{name} mismatch between refine and imply for spot={spot}, is_call={is_call}, target_vol={target_vol}, t={t}: refined={refined:.10}, implied={implied:.10}, abs_diff={abs_diff:.2e}, rel_diff={rel_diff:.2e}, max_allowed={max_allowed:.2e}"
);
};
check_6_sig_figs(refined_result.price, implied_result.price, "Price", false);
check_6_sig_figs(refined_result.delta, implied_result.delta, "Delta", false);
check_6_sig_figs(refined_result.gamma, implied_result.gamma, "Gamma", true);
check_6_sig_figs(refined_result.vega, implied_result.vega, "Vega", false);
check_6_sig_figs(refined_result.theta, implied_result.theta, "Theta", false);
}
}