use crate::constants::{TRADING_DAYS, ZERO};
use crate::error::greeks::GreeksError;
use crate::greeks::utils::{big_n, d1, d2, n};
use crate::model::types::{OptionStyle, OptionType};
use crate::{Options, Side};
use positive::Positive;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::{Decimal, MathematicalOps};
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(PartialEq, Clone, DebugPretty, DisplaySimple, ToSchema, Serialize)]
pub struct Greek {
pub delta: Decimal,
pub gamma: Decimal,
pub theta: Decimal,
pub vega: Decimal,
pub rho: Decimal,
pub rho_d: Decimal,
pub alpha: Decimal,
pub vanna: Decimal,
pub vomma: Decimal,
pub veta: Decimal,
pub charm: Decimal,
pub color: Decimal,
}
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct GreeksSnapshot {
pub delta: Decimal,
pub gamma: Decimal,
pub theta: Decimal,
pub vega: Decimal,
pub rho: Option<Decimal>,
pub rho_d: Option<Decimal>,
pub alpha: Option<Decimal>,
pub vanna: Decimal,
pub vomma: Decimal,
pub veta: Decimal,
pub charm: Decimal,
pub color: Decimal,
}
pub trait Greeks {
fn get_options(&self) -> Result<Vec<&Options>, GreeksError>;
fn greeks(&self) -> Result<Greek, GreeksError> {
let delta = self.delta()?;
let gamma = self.gamma()?;
let theta = self.theta()?;
let vega = self.vega()?;
let rho = self.rho()?;
let rho_d = self.rho_d()?;
let alpha = self.alpha()?;
let vanna = self.vanna()?;
let vomma = self.vomma()?;
let veta = self.veta()?;
let charm = self.charm()?;
let color = self.color()?;
Ok(Greek {
delta,
gamma,
theta,
vega,
rho,
rho_d,
alpha,
vanna,
vomma,
veta,
charm,
color,
})
}
fn delta(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut delta_value = Decimal::ZERO;
for option in options {
delta_value += delta(option)?;
}
Ok(delta_value)
}
fn gamma(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut gamma_value = Decimal::ZERO;
for option in options {
gamma_value += gamma(option)?;
}
Ok(gamma_value)
}
fn theta(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut theta_value = Decimal::ZERO;
for option in options {
theta_value += theta(option)?;
}
Ok(theta_value)
}
fn vega(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut vega_value = Decimal::ZERO;
for option in options {
vega_value += vega(option)?;
}
Ok(vega_value)
}
fn rho(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut rho_value = Decimal::ZERO;
for option in options {
rho_value += rho(option)?;
}
Ok(rho_value)
}
fn rho_d(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut rho_d_value = Decimal::ZERO;
for option in options {
rho_d_value += rho_d(option)?;
}
Ok(rho_d_value)
}
fn alpha(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut alpha_value = Decimal::ZERO;
for option in options {
alpha_value += alpha(option)?;
}
Ok(alpha_value)
}
fn vanna(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut vanna_value = Decimal::ZERO;
for option in options {
vanna_value += vanna(option)?;
}
Ok(vanna_value)
}
fn vomma(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut vomma_value = Decimal::ZERO;
for option in options {
vomma_value += vomma(option)?;
}
Ok(vomma_value)
}
fn veta(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut veta_value = Decimal::ZERO;
for option in options {
veta_value += veta(option)?;
}
Ok(veta_value)
}
fn charm(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut charm_value = Decimal::ZERO;
for option in options {
charm_value += charm(option)?;
}
Ok(charm_value)
}
fn color(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut color_value = Decimal::ZERO;
for option in options {
color_value += color(option)?;
}
Ok(color_value)
}
}
pub fn delta(option: &Options) -> Result<Decimal, GreeksError> {
if !matches!(option.option_type, OptionType::European) {
return crate::greeks::numerical::numerical_delta(option);
}
let expiration_date = option.expiration_date.get_years()?;
if expiration_date == Decimal::ZERO {
return match (
&option.option_style,
&option.side,
&option.strike_price,
&option.underlying_price,
) {
(OptionStyle::Call, Side::Long, strike, price) if price > strike => Ok(Decimal::ONE),
(OptionStyle::Call, Side::Long, _, _) => Ok(Decimal::ZERO),
(OptionStyle::Call, Side::Short, strike, price) if price > strike => Ok(-Decimal::ONE),
(OptionStyle::Call, Side::Short, _, _) => Ok(Decimal::ZERO),
(OptionStyle::Put, Side::Long, strike, price) if price < strike => Ok(-Decimal::ONE),
(OptionStyle::Put, Side::Long, _, _) => Ok(Decimal::ZERO),
(OptionStyle::Put, Side::Short, strike, price) if price < strike => Ok(Decimal::ONE),
(OptionStyle::Put, Side::Short, _, _) => Ok(Decimal::ZERO),
};
}
let dividend_yield: Positive = option.dividend_yield;
let sign = if option.is_long() {
Decimal::ONE
} else {
Decimal::NEGATIVE_ONE
};
if option.implied_volatility == ZERO {
return match option.option_style {
OptionStyle::Call => {
if option.underlying_price >= option.strike_price {
Ok(sign) } else {
Ok(Decimal::ZERO) }
}
OptionStyle::Put => {
if option.underlying_price <= option.strike_price {
Ok(sign * Decimal::NEGATIVE_ONE) } else {
Ok(Decimal::ZERO) }
}
};
}
let d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
expiration_date,
option.implied_volatility,
)?;
let div_date = (-expiration_date.to_dec() * dividend_yield).exp();
let delta = match option.option_style {
OptionStyle::Call => sign * big_n(d1)? * div_date,
OptionStyle::Put => sign * (big_n(d1)? - Decimal::ONE) * div_date,
};
let delta: Decimal = delta.clamp(Decimal::NEGATIVE_ONE, Decimal::ONE);
let quantity: Decimal = option.quantity.into();
Ok(delta * quantity)
}
pub fn gamma(option: &Options) -> Result<Decimal, GreeksError> {
if !matches!(option.option_type, OptionType::European) {
return crate::greeks::numerical::numerical_gamma(option);
}
if option.implied_volatility == ZERO {
return Ok(Decimal::ZERO);
}
let expiration_date: Positive = option.expiration_date.get_years()?;
if expiration_date == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
expiration_date,
option.implied_volatility,
)?;
let dividend_yield: Decimal = option.dividend_yield.into();
let underlying_price: Decimal = option.underlying_price.into();
let implied_volatility: Positive = option.implied_volatility;
let gamma: Decimal = (expiration_date.to_dec() * -dividend_yield).exp() * n(d1)?
/ (underlying_price * implied_volatility * expiration_date.sqrt().to_dec());
let quantity: Decimal = option.quantity.into();
Ok(gamma * quantity)
}
pub fn theta(option: &Options) -> Result<Decimal, GreeksError> {
let t = option.expiration_date.get_years()?;
if t == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
t,
option.implied_volatility,
)?;
let d2 = d2(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
t,
option.implied_volatility,
)?;
let s = option.underlying_price.to_dec();
let k = option.strike_price.to_dec();
let r = option.risk_free_rate;
let q = option.dividend_yield.to_dec();
let sigma = option.implied_volatility.to_dec();
let common_term = -(s * n(d1)? * sigma) / (Decimal::TWO * t.sqrt());
let exp_minus_rt = (-r * t).exp();
let exp_minus_qt = (-q * t).exp();
let theta = match option.option_style {
OptionStyle::Call => {
common_term - r * k * exp_minus_rt * big_n(d2)? + q * s * exp_minus_qt * big_n(d1)?
}
OptionStyle::Put => {
common_term + r * k * exp_minus_rt * big_n(-d2)? - q * s * exp_minus_qt * big_n(-d1)?
}
};
Ok((theta * option.quantity.to_dec()) / Decimal::from(365))
}
pub fn vega(option: &Options) -> Result<Decimal, GreeksError> {
let expiration_date: Positive = option.expiration_date.get_years()?;
if expiration_date == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
expiration_date,
option.implied_volatility,
)?;
let dividend_yield: Positive = option.dividend_yield;
let underlying_price: Decimal = option.underlying_price.to_dec();
let vega: Decimal = underlying_price
* (-expiration_date.to_dec() * dividend_yield).exp()
* n(d1)?
* expiration_date.sqrt()
/ Decimal::ONE_HUNDRED;
let quantity: Decimal = option.quantity.into();
Ok(vega * quantity)
}
pub fn rho(option: &Options) -> Result<Decimal, GreeksError> {
let t = option.expiration_date.get_years()?;
if t == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let d2 = d2(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
t,
option.implied_volatility,
)?;
let k = option.strike_price.to_dec();
let r = option.risk_free_rate;
let e_rt = (-r * t).exp();
let base_rho = k * t * e_rt;
let rho = match option.option_style {
OptionStyle::Call => {
let n_d2 = big_n(d2)?;
base_rho * n_d2
}
OptionStyle::Put => {
let n_minus_d2 = big_n(-d2)?;
-base_rho * n_minus_d2
}
};
Ok((rho * option.quantity.to_dec()) / Decimal::from(100))
}
pub fn rho_d(option: &Options) -> Result<Decimal, GreeksError> {
let expiration_date: Positive = option.expiration_date.get_years()?;
let d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
expiration_date,
option.implied_volatility,
)?;
let dividend_yield: Positive = option.dividend_yield;
let underlying_price: Decimal = option.underlying_price.to_dec();
let rhod = match option.option_style {
OptionStyle::Call => {
-expiration_date.to_dec()
* underlying_price
* (-expiration_date.to_dec() * dividend_yield).exp()
* big_n(d1)?
}
OptionStyle::Put => {
expiration_date.to_dec()
* underlying_price
* (-expiration_date.to_dec() * dividend_yield).exp()
* big_n(-d1)?
}
};
let quantity: Decimal = option.quantity.into();
Ok(rhod * quantity / Decimal::from(100))
}
pub fn alpha(option: &Options) -> Result<Decimal, GreeksError> {
let gamma = gamma(option)?;
let theta = theta(option)?;
match (gamma, theta) {
(val, _) if val == Decimal::ZERO => Ok(Decimal::ZERO),
(_, val) if val == Decimal::ZERO => Ok(Decimal::MAX),
_ => Ok(gamma / theta),
}
}
pub fn vanna(option: &Options) -> Result<Decimal, GreeksError> {
if option.implied_volatility == ZERO {
return Ok(Decimal::ZERO);
}
let expiration_date: Positive = option.expiration_date.get_years()?;
let d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
expiration_date,
option.implied_volatility,
)?;
let d2 = d2(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
expiration_date,
option.implied_volatility,
)?;
let dividend_yield: Decimal = option.dividend_yield.into();
let implied_volatility: Positive = option.implied_volatility;
let n_d1: Decimal = n(d1)?;
let e_rt: Decimal = -(expiration_date.to_dec() * -dividend_yield).exp();
let vanna: Decimal = e_rt * n_d1 * (d2 / implied_volatility);
let quantity: Decimal = option.quantity.into();
Ok(vanna * quantity)
}
pub fn vomma(option: &Options) -> Result<Decimal, GreeksError> {
let expiration_date: Positive = option.expiration_date.get_years()?;
if expiration_date == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
expiration_date,
option.implied_volatility,
)?;
let d2 = d2(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
expiration_date,
option.implied_volatility,
)?;
let vega = vega(option)?;
let implied_volatility: Positive = option.implied_volatility;
let vomma: Decimal = vega * (d1 * d2 / implied_volatility);
let quantity: Decimal = option.quantity.into();
Ok(vomma * quantity)
}
pub fn veta(option: &Options) -> Result<Decimal, GreeksError> {
let expiration_date: Positive = option.expiration_date.get_years()?;
if expiration_date == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
expiration_date,
option.implied_volatility,
)?;
let d2 = d2(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
expiration_date,
option.implied_volatility,
)?;
let vega = vega(option)?;
let implied_volatility: Positive = option.implied_volatility;
let dividend_yield: Decimal = option.dividend_yield.into();
let risk_free_rate: Decimal = option.risk_free_rate;
let add1 =
(risk_free_rate - dividend_yield) * d1 / (implied_volatility * expiration_date.sqrt());
let add2 = (Decimal::ONE + d1 * d2) / (Decimal::TWO * expiration_date);
let veta: Decimal = -vega * (dividend_yield + add1 - add2);
let veta_adj: Decimal = veta / (TRADING_DAYS * Decimal::ONE_HUNDRED);
let quantity: Decimal = option.quantity.into();
Ok(veta_adj * quantity)
}
pub fn charm(option: &Options) -> Result<Decimal, GreeksError> {
let tau = option.expiration_date.get_years()?;
if tau == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
tau, option.implied_volatility,
)?;
let d2 = d2(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
tau, option.implied_volatility,
)?;
let r = option.risk_free_rate;
let q = option.dividend_yield.to_dec();
let sigma = option.implied_volatility;
let exp_minus_qt = (-q * tau).exp();
let common_term = (Decimal::TWO * (r - q) * tau - d2 * sigma * tau.sqrt())
/ (Decimal::TWO * tau * sigma * tau.sqrt());
let charm = match option.option_style {
OptionStyle::Call => {
(q * exp_minus_qt * big_n(d1)?) - (exp_minus_qt * n(d1)? * common_term)
}
OptionStyle::Put => {
(-q * exp_minus_qt * big_n(-d1)?) - (exp_minus_qt * n(d1)? * common_term)
}
};
Ok((charm * option.quantity) / Decimal::from(365))
}
pub fn color(option: &Options) -> Result<Decimal, GreeksError> {
let tau = option.expiration_date.get_years()?;
if tau == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
tau, option.implied_volatility,
)?;
let d2 = d2(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
tau, option.implied_volatility,
)?;
let r = option.risk_free_rate;
let s = option.underlying_price;
let q = option.dividend_yield.to_dec();
let sigma = option.implied_volatility;
let exp_minus_qt = (-q * tau).exp();
let factor1 = n(d1)? / (Decimal::TWO * s * tau * sigma * tau.sqrt());
let numerator = (Decimal::TWO * (r - q) * tau) - (d2 * sigma * tau.sqrt());
let denominator = sigma * tau.sqrt();
let factor2 = (Decimal::TWO * q * tau) + Decimal::ONE + ((numerator / denominator) * d1);
let color = (-exp_minus_qt * factor1 * factor2 * option.quantity) / Decimal::from(365);
Ok(color)
}
#[cfg(test)]
pub mod tests_delta_equations {
use super::*;
use crate::constants::ZERO;
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option;
use crate::strategies::DELTA_THRESHOLD;
use positive::constants::DAYS_IN_A_YEAR;
use crate::{ExpirationDate, assert_decimal_eq};
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
use tracing::info;
#[test]
fn test_delta_no_volatility_itm() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0),
Positive::ONE,
pos_or_panic!(150.0),
Positive::ZERO,
);
let delta_value = delta(&option).unwrap();
info!("Zero Volatility: {}", delta_value);
assert_relative_eq!(delta_value.to_f64().unwrap(), 1.0, epsilon = 1e-8);
}
#[test]
fn test_delta_no_volatility_otm() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(110.0),
Positive::ONE,
pos_or_panic!(150.0),
Positive::ZERO,
);
let delta_value = delta(&option).unwrap().to_f64().unwrap();
info!("Zero Volatility: {}", delta_value);
assert_relative_eq!(delta_value, ZERO, epsilon = 1e-8);
}
#[test]
fn test_delta_no_volatility_itm_put() {
let option = create_sample_option(
OptionStyle::Put,
Side::Long,
pos_or_panic!(150.0),
Positive::ONE,
pos_or_panic!(150.0),
Positive::ZERO,
);
let delta_value = delta(&option).unwrap().to_f64().unwrap();
info!("Zero Volatility: {}", delta_value);
assert_relative_eq!(delta_value, -1.0, epsilon = 1e-8);
}
#[test]
fn test_delta_no_volatility_otm_put() {
let option = create_sample_option(
OptionStyle::Put,
Side::Long,
pos_or_panic!(160.0),
Positive::ONE,
pos_or_panic!(150.0),
Positive::ZERO,
);
let delta_value = delta(&option).unwrap().to_f64().unwrap();
info!("Zero Volatility: {}", delta_value);
assert_relative_eq!(delta_value, ZERO, epsilon = 1e-8);
}
#[test]
fn test_delta_no_volatility_itm_short() {
let option = create_sample_option(
OptionStyle::Call,
Side::Short,
pos_or_panic!(150.0),
Positive::ONE,
pos_or_panic!(150.0),
Positive::ZERO,
);
let delta_value = delta(&option).unwrap().to_f64().unwrap();
info!("Zero Volatility: {}", delta_value);
assert_relative_eq!(delta_value, -1.0, epsilon = 1e-8);
}
#[test]
fn test_delta_no_volatility_otm_short() {
let option = create_sample_option(
OptionStyle::Call,
Side::Short,
pos_or_panic!(110.0),
Positive::ONE,
pos_or_panic!(150.0),
Positive::ZERO,
);
let delta_value = delta(&option).unwrap().to_f64().unwrap();
info!("Zero Volatility: {}", delta_value);
assert_relative_eq!(delta_value, ZERO, epsilon = 1e-8);
}
#[test]
fn test_delta_no_volatility_itm_put_short() {
let option = create_sample_option(
OptionStyle::Put,
Side::Short,
pos_or_panic!(150.0),
Positive::ONE,
pos_or_panic!(150.0),
Positive::ZERO,
);
let delta_value = delta(&option).unwrap().to_f64().unwrap();
info!("Zero Volatility: {}", delta_value);
assert_relative_eq!(delta_value, 1.0, epsilon = 1e-8);
}
#[test]
fn test_delta_no_volatility_otm_put_short() {
let option = create_sample_option(
OptionStyle::Put,
Side::Short,
pos_or_panic!(160.0),
Positive::ONE,
pos_or_panic!(150.0),
Positive::ZERO,
);
let delta_value = delta(&option).unwrap().to_f64().unwrap();
info!("Zero Volatility: {}", delta_value);
assert_relative_eq!(delta_value, ZERO, epsilon = 1e-8);
}
#[test]
fn test_delta_deep_in_the_money_call() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0),
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.20),
);
let delta_value = delta(&option).unwrap().to_f64().unwrap();
info!("Deep ITM Call Delta: {}", delta_value);
assert_relative_eq!(delta_value, 0.9991784198733309, epsilon = 1e-8);
}
#[test]
fn test_delta_deep_out_of_the_money_call() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(50.0),
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.20),
);
let delta_value = delta(&option).unwrap().to_f64().unwrap();
info!("Deep OTM Call Delta: {}", delta_value);
assert_relative_eq!(delta_value, 2.0418256951423236e-33, epsilon = 1e-4);
}
#[test]
fn test_delta_at_the_money_put() {
let option = create_sample_option(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.20),
);
let delta_value = delta(&option).unwrap();
info!("ATM Put Delta: {}", delta_value);
assert_decimal_eq!(delta_value, dec!(-0.459658497), DELTA_THRESHOLD);
}
#[test]
fn test_delta_short_term_high_volatility() {
let mut option = create_sample_option(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.50),
);
option.expiration_date = ExpirationDate::Days(pos_or_panic!(7.0));
let delta_value = delta(&option).unwrap().to_f64().unwrap();
info!("Short-term High Vol Call Delta: {}", delta_value);
assert_relative_eq!(delta_value, 0.519229469584234, epsilon = 1e-4);
}
#[test]
fn test_delta_long_term_low_volatility() {
let mut option = create_sample_option(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.10),
);
option.expiration_date = ExpirationDate::Days(DAYS_IN_A_YEAR);
let delta_value = delta(&option).unwrap();
info!("Long-term Low Vol Put Delta: {}", delta_value);
assert_decimal_eq!(delta_value, dec!(-0.2882625996), DELTA_THRESHOLD);
}
#[test]
fn test_delta_long_almost_zero_time_to_maturity() {
let mut option = create_sample_option(
OptionStyle::Call,
Side::Short,
pos_or_panic!(21637.0),
Positive::ONE,
pos_or_panic!(21825.0),
pos_or_panic!(0.219),
);
option.expiration_date = ExpirationDate::Days(Positive::ONE);
let delta_value = delta(&option).unwrap();
info!("Long-term Low Vol Put Delta: {}", delta_value);
assert_decimal_eq!(delta_value, dec!(-0.230544), DELTA_THRESHOLD);
}
}
#[cfg(test)]
pub mod tests_gamma_equations {
use super::*;
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option;
use positive::constants::DAYS_IN_A_YEAR;
use crate::ExpirationDate;
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use positive::pos_or_panic;
use tracing::info;
#[test]
fn test_gamma_deep_in_the_money_call() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0),
Positive::ONE,
pos_or_panic!(120.0),
pos_or_panic!(0.2),
);
let gamma_value = gamma(&option).unwrap().to_f64().unwrap();
info!("Deep ITM Call Gamma: {}", gamma_value);
assert_relative_eq!(gamma_value, 0.000016049457791525, epsilon = 1e-8);
}
#[test]
fn test_gamma_deep_out_of_the_money_call() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(50.0),
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.20),
);
let gamma_value = gamma(&option).unwrap().to_f64().unwrap();
info!("Deep OTM Call Gamma: {}", gamma_value);
assert_relative_eq!(gamma_value, 0.0, epsilon = 1e-34);
}
#[test]
fn test_gamma_at_the_money_put() {
let option = create_sample_option(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.20),
);
let gamma_value = gamma(&option).unwrap().to_f64().unwrap();
info!("ATM Put Gamma: {}", gamma_value);
assert_relative_eq!(gamma_value, 0.06917076441486919, epsilon = 1e-8);
}
#[test]
fn test_gamma_short_term_high_volatility() {
let mut option = create_sample_option(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.50),
);
option.expiration_date = ExpirationDate::Days(pos_or_panic!(7.0));
let gamma_value = gamma(&option).unwrap().to_f64().unwrap();
info!("Short-term High Vol Call Gamma: {}", gamma_value);
assert_relative_eq!(gamma_value, 0.05753657912620555, epsilon = 1e-8);
}
#[test]
fn test_gamma_long_term_low_volatility() {
let mut option = create_sample_option(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.10),
);
option.expiration_date = ExpirationDate::Days(DAYS_IN_A_YEAR);
let gamma_value = gamma(&option).unwrap().to_f64().unwrap();
info!("Long-term Low Vol Put Gamma: {}", gamma_value);
assert_relative_eq!(gamma_value, 0.033953150664723986, epsilon = 1e-8);
}
#[test]
fn test_gamma_zero_volatility() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
Positive::ZERO,
);
let gamma_value = gamma(&option).unwrap().to_f64().unwrap();
info!("Zero Volatility Call Gamma: {}", gamma_value);
assert_relative_eq!(gamma_value, 0.0, epsilon = 1e-8);
}
#[test]
fn test_gamma_extreme_high_volatility() {
let option = create_sample_option(
OptionStyle::Put,
Side::Short,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(5.0),
);
let gamma_value = gamma(&option).unwrap().to_f64().unwrap();
info!("Extreme High Volatility Put Gamma: {}", gamma_value);
assert_relative_eq!(gamma_value, 0.002146478293943308, epsilon = 1e-8);
}
}
#[cfg(test)]
mod tests_gamma_equations_values {
use super::*;
use crate::model::types::{OptionStyle, Side};
use crate::{ExpirationDate, OptionType};
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use positive::pos_or_panic;
use tracing::info;
#[test]
fn test_50_vol_10() {
let option = Options::new(
OptionType::European,
Side::Long,
"XYZ".parse().unwrap(),
pos_or_panic!(50.0),
ExpirationDate::Days(pos_or_panic!(365.0)),
pos_or_panic!(0.10),
Positive::ONE,
pos_or_panic!(50.0),
Decimal::ZERO,
OptionStyle::Call,
Positive::ZERO,
None,
);
let gamma_value = gamma(&option).unwrap().to_f64().unwrap();
info!("Gamma: {}", gamma_value);
assert_relative_eq!(gamma_value, 0.0796887828189609, epsilon = 1e-8);
}
#[test]
fn test_50_vol_5() {
let option = Options::new(
OptionType::European,
Side::Long,
"XYZ".parse().unwrap(),
pos_or_panic!(50.0),
ExpirationDate::Days(pos_or_panic!(365.0)),
pos_or_panic!(0.05),
Positive::ONE,
pos_or_panic!(50.0),
Decimal::ZERO,
OptionStyle::Call,
Positive::ZERO,
None,
);
let gamma_value = gamma(&option).unwrap().to_f64().unwrap();
info!("Gamma: {}", gamma_value);
assert_relative_eq!(gamma_value, 0.15952705216736393, epsilon = 1e-8);
}
#[test]
fn test_50_vol_20() {
let option = Options::new(
OptionType::European,
Side::Long,
"XYZ".parse().unwrap(),
pos_or_panic!(50.0),
ExpirationDate::Days(pos_or_panic!(365.0)),
pos_or_panic!(0.2),
Positive::ONE,
pos_or_panic!(50.0),
Decimal::ZERO,
OptionStyle::Call,
Positive::ZERO,
None,
);
let gamma_value = gamma(&option).unwrap().to_f64().unwrap();
info!("Gamma: {}", gamma_value);
assert_relative_eq!(gamma_value, 0.03969525474873078, epsilon = 1e-8);
}
}
#[cfg(test)]
pub mod tests_vega_equation {
use super::*;
use crate::ExpirationDate;
use crate::model::types::{OptionType, Side};
use num_traits::ToPrimitive;
use positive::constants::DAYS_IN_A_YEAR;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
fn create_test_option(
underlying_price: Positive,
strike_price: Positive,
implied_volatility: Positive,
dividend_yield: Positive,
expiration_in_days: Positive,
) -> Options {
Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
strike_price,
ExpirationDate::Days(expiration_in_days),
implied_volatility,
Positive::ONE, underlying_price,
dec!(0.05), OptionStyle::Call,
dividend_yield,
None, )
}
#[test]
fn test_vega_atm() {
let option = create_test_option(
Positive::HUNDRED,
Positive::HUNDRED,
pos_or_panic!(0.2),
Positive::ZERO,
DAYS_IN_A_YEAR,
);
let vega = vega(&option).unwrap().to_f64().unwrap();
let expected_vega = 0.3752403469;
assert!(
(vega - expected_vega).abs() < 1e-5,
"Vega ATM test failed: expected {expected_vega}, got {vega}"
);
}
#[test]
fn test_vega_otm() {
let option = create_test_option(
pos_or_panic!(90.0),
Positive::HUNDRED,
pos_or_panic!(0.2),
Positive::ZERO,
DAYS_IN_A_YEAR,
);
let vega = vega(&option).unwrap().to_f64().unwrap();
let expected_vega = 0.35347991;
assert!(
(vega - expected_vega).abs() < 1e-5,
"Vega OTM test failed: expected {expected_vega}, got {vega}"
);
}
#[test]
fn test_vega_short_expiration() {
let option = create_test_option(
Positive::HUNDRED,
Positive::HUNDRED,
pos_or_panic!(0.2),
Positive::ZERO,
Positive::ONE,
);
let vega = vega(&option).unwrap().to_f64().unwrap();
let expected_vega = 0.020878089;
assert!(
(vega - expected_vega).abs() < 1e-5,
"Vega short expiration test failed: expected {expected_vega}, got {vega}"
);
}
#[test]
fn test_vega_with_dividends() {
let option = create_test_option(
Positive::HUNDRED,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(0.03),
Positive::ONE,
);
let vega = vega(&option).unwrap().to_f64().unwrap();
let expected_vega = 0.0208763735;
assert!(
(vega - expected_vega).abs() < 1e-5,
"Vega with dividends test failed: expected {expected_vega}, got {vega}"
);
}
#[test]
fn test_vega_itm() {
let option = create_test_option(
pos_or_panic!(110.0),
Positive::HUNDRED,
pos_or_panic!(0.2),
Positive::ZERO,
Positive::ONE,
);
let vega = vega(&option).unwrap().to_f64().unwrap();
let expected_vega = 0.0;
assert!(
(vega - expected_vega).abs() < 1e-5,
"Vega ITM test failed: expected {expected_vega}, got {vega}"
);
}
}
#[cfg(test)]
pub mod tests_rho_equations {
use super::*;
use crate::model::types::{OptionStyle, OptionType, Side};
use crate::{ExpirationDate, assert_decimal_eq};
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use positive::constants::DAYS_IN_A_YEAR;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
fn create_test_option(style: OptionStyle) -> Options {
Options {
option_type: OptionType::European,
side: Side::Long,
underlying_symbol: "TEST".to_string(),
strike_price: Positive::HUNDRED,
expiration_date: ExpirationDate::Days(DAYS_IN_A_YEAR),
implied_volatility: pos_or_panic!(0.2),
quantity: Positive::ONE,
underlying_price: Positive::HUNDRED,
risk_free_rate: dec!(0.05),
option_style: style,
dividend_yield: Positive::ZERO,
exotic_params: None,
}
}
#[test]
fn test_rho_call_option() {
let option = create_test_option(OptionStyle::Call);
let result = rho(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(result, 0.532324815464, epsilon = 1e-8);
}
#[test]
fn test_rho_put_option() {
let option = create_test_option(OptionStyle::Put);
let result = rho(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(result, -0.41890460905, epsilon = 1e-8);
}
#[test]
fn test_rho_zero_time_to_expiry() {
let mut option = create_test_option(OptionStyle::Call);
option.expiration_date = ExpirationDate::Days(Positive::ZERO);
let result = rho(&option).is_ok();
assert!(result);
assert_decimal_eq!(rho(&option).unwrap(), Decimal::ZERO, dec!(1e-8));
}
#[test]
fn test_rho_zero_risk_free_rate() {
let mut option = create_test_option(OptionStyle::Call);
option.risk_free_rate = dec!(0.0);
let result = rho(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(result, 0.460172162, epsilon = 1e-8);
}
#[test]
fn test_rho_deep_out_of_money_call() {
let mut option = create_test_option(OptionStyle::Call);
option.strike_price = pos_or_panic!(1000.0);
let result = rho(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(result, 0.0, epsilon = 1e-8);
}
#[test]
fn test_rho_deep_out_of_money_put() {
let mut option = create_test_option(OptionStyle::Put);
option.strike_price = Positive::ONE;
let result = rho(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(result, 0.0, epsilon = 1e-8);
}
#[test]
fn test_rho_high_volatility() {
let mut option = create_test_option(OptionStyle::Call);
option.implied_volatility = Positive::ONE;
let result = rho(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(result, 0.3104386883, epsilon = 0.0001);
}
}
#[cfg(test)]
pub mod tests_theta_long_equations {
use super::*;
use crate::ExpirationDate;
use crate::model::types::Side;
use crate::model::utils::create_sample_option;
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use positive::constants::DAYS_IN_A_YEAR;
use positive::pos_or_panic;
#[test]
fn test_theta_call_option() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0), Positive::ONE, pos_or_panic!(155.0), pos_or_panic!(0.20), );
let expected_theta = -0.0561725050;
let calculated_theta = theta(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(calculated_theta, expected_theta, epsilon = 1e-8);
}
#[test]
fn test_theta_put_option() {
let option = create_sample_option(
OptionStyle::Put,
Side::Long,
pos_or_panic!(150.0), Positive::ONE, pos_or_panic!(145.0), pos_or_panic!(0.25), );
let expected_theta = -0.055928204732;
let calculated_theta = theta(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(calculated_theta, expected_theta, epsilon = 1e-8);
}
#[test]
fn test_theta_call_option_near_expiry() {
let mut option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0), Positive::ONE, pos_or_panic!(150.0), pos_or_panic!(0.15), );
option.expiration_date = ExpirationDate::Days(Positive::ONE);
let expected_theta = -0.24315788969;
let calculated_theta = theta(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(calculated_theta, expected_theta, epsilon = 1e-8);
}
#[test]
fn test_theta_put_option_far_from_expiry() {
let mut option = create_sample_option(
OptionStyle::Put,
Side::Long,
pos_or_panic!(140.0), Positive::ONE, pos_or_panic!(130.0), pos_or_panic!(0.30), );
option.expiration_date = ExpirationDate::Days(DAYS_IN_A_YEAR);
let expected_theta = -0.0139607780805;
let calculated_theta = theta(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(calculated_theta, expected_theta, epsilon = 1e-8);
}
}
#[cfg(test)]
pub mod tests_theta_short_equations {
use super::*;
use crate::ExpirationDate;
use crate::model::types::Side;
use crate::model::utils::create_sample_option;
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use positive::constants::DAYS_IN_A_YEAR;
use positive::pos_or_panic;
#[test]
fn test_theta_short_call_option() {
let option = create_sample_option(
OptionStyle::Call,
Side::Short,
pos_or_panic!(150.0), Positive::ONE, pos_or_panic!(155.0), pos_or_panic!(0.20), );
let expected_theta = -0.05617250509;
let calculated_theta = theta(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(calculated_theta, expected_theta, epsilon = 1e-8);
}
#[test]
fn test_theta_short_put_option() {
let option = create_sample_option(
OptionStyle::Put,
Side::Short,
pos_or_panic!(150.0), Positive::ONE, pos_or_panic!(145.0), pos_or_panic!(0.25), );
let expected_theta = -0.05592820473;
let calculated_theta = theta(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(calculated_theta, expected_theta, epsilon = 1e-8);
}
#[test]
fn test_theta_short_call_option_near_expiry() {
let mut option = create_sample_option(
OptionStyle::Call,
Side::Short,
pos_or_panic!(150.0), Positive::ONE, pos_or_panic!(150.0), pos_or_panic!(0.15), );
option.expiration_date = ExpirationDate::Days(Positive::ONE);
let expected_theta = -0.2431578896;
let calculated_theta = theta(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(calculated_theta, expected_theta, epsilon = 1e-8);
}
#[test]
fn test_theta_short_put_option_far_from_expiry() {
let mut option = create_sample_option(
OptionStyle::Put,
Side::Short,
pos_or_panic!(140.0), Positive::ONE, pos_or_panic!(130.0), pos_or_panic!(0.30), );
option.expiration_date = ExpirationDate::Days(DAYS_IN_A_YEAR);
let expected_theta = -0.01396077;
let calculated_theta = theta(&option).unwrap().to_f64().unwrap();
assert_relative_eq!(calculated_theta, expected_theta, epsilon = 1e-8);
}
}
#[cfg(test)]
mod tests_greeks_trait {
use super::*;
use crate::model::types::{OptionStyle, OptionType, Side};
use crate::{ExpirationDate, assert_decimal_eq};
use positive::pos_or_panic;
use rust_decimal_macros::dec;
struct TestOptionCollection {
options: Vec<Options>,
}
impl Greeks for TestOptionCollection {
fn get_options(&self) -> Result<Vec<&Options>, GreeksError> {
Ok(self.options.iter().collect())
}
}
fn create_test_option(side: Side, style: OptionStyle, quantity: Positive) -> Options {
Options::new(
OptionType::European,
side,
"TEST".to_string(),
Positive::HUNDRED, ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2), quantity,
Positive::HUNDRED, dec!(0.05), style,
pos_or_panic!(0.01), None, )
}
#[test]
fn test_greeks_single_option() {
let option = create_test_option(Side::Long, OptionStyle::Call, Positive::ONE);
let collection = TestOptionCollection {
options: vec![option],
};
let greeks = collection.greeks().unwrap();
assert_decimal_eq!(greeks.delta, dec!(0.539519922), dec!(0.000001));
assert_decimal_eq!(greeks.gamma, dec!(0.069170764), dec!(0.000001));
assert_decimal_eq!(greeks.theta, dec!(-0.04351001), dec!(0.000001));
assert_decimal_eq!(greeks.vega, dec!(0.1137053), dec!(0.000001));
assert_decimal_eq!(greeks.rho, dec!(0.04233121458), dec!(0.000001));
assert_decimal_eq!(greeks.rho_d, dec!(-0.04434410), dec!(0.000001));
assert_decimal_eq!(greeks.vanna, dec!(-0.08527902), dec!(0.000001));
assert_decimal_eq!(greeks.vomma, dec!(0.00245323), dec!(0.000001));
assert_decimal_eq!(greeks.veta, dec!(0.00002720), dec!(0.000001));
}
#[test]
fn test_greeks_multiple_options() {
let option1 = create_test_option(Side::Long, OptionStyle::Call, Positive::ONE);
let option2 = create_test_option(Side::Short, OptionStyle::Put, Positive::ONE);
let collection = TestOptionCollection {
options: vec![option1, option2],
};
let greeks = collection.greeks().unwrap();
assert!(
greeks.delta.abs() > dec!(0.0),
"Delta should be non-zero for multiple options"
);
assert!(
greeks.gamma.abs() > dec!(0.0),
"Gamma should be non-zero for multiple options"
);
assert!(
greeks.theta.abs() > dec!(0.0),
"Theta should be non-zero for multiple options"
);
assert!(
greeks.vega.abs() > dec!(0.0),
"Vega should be non-zero for multiple options"
);
assert!(
greeks.rho.abs() > dec!(0.0),
"Rho should be non-zero for multiple options"
);
assert!(
greeks.rho_d.abs() > dec!(0.0),
"Rho_d should be non-zero for multiple options"
);
assert!(
greeks.vanna.abs() > dec!(0.0),
"Vanna should be non-zero for multiple options"
);
assert!(
greeks.vomma.abs() > dec!(0.0),
"Vomma should be non-zero for multiple options"
);
assert!(
greeks.veta.abs() > dec!(0.0),
"Veta should be non-zero for multiple options"
);
}
#[test]
fn test_greeks_simple_validation() {
let option = Options::new(
OptionType::European,
Side::Long,
"AAPL".to_string(),
pos_or_panic!(155.0),
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.20),
Positive::ONE,
pos_or_panic!(150.0),
dec!(0.05),
OptionStyle::Call,
pos_or_panic!(0.00),
None,
);
let greeks = option.greeks().unwrap();
assert_decimal_eq!(greeks.delta, dec!(0.3186329), dec!(0.000001));
assert_decimal_eq!(greeks.gamma, dec!(0.0415044), dec!(0.000001));
assert_decimal_eq!(greeks.theta, dec!(-0.0574808), dec!(0.000001));
assert_decimal_eq!(greeks.vega, dec!(0.15350973), dec!(0.000001));
assert_decimal_eq!(greeks.rho, dec!(0.03786580), dec!(0.000001));
assert_decimal_eq!(greeks.rho_d, dec!(-0.03928351), dec!(0.000001));
assert_decimal_eq!(greeks.vanna, dec!(0.94393865), dec!(0.000001));
assert_decimal_eq!(greeks.vomma, dec!(0.19140525), dec!(0.000001));
assert_decimal_eq!(greeks.veta, dec!(0.00004880), dec!(0.000001));
}
#[test]
fn test_greeks_zero_quantity() {
let option = create_test_option(Side::Long, OptionStyle::Call, Positive::ZERO);
let collection = TestOptionCollection {
options: vec![option],
};
let greeks = collection.greeks().unwrap();
assert_eq!(greeks.delta, dec!(0.0));
assert_eq!(greeks.gamma, dec!(0.0));
assert_eq!(greeks.theta, dec!(0.0));
assert_eq!(greeks.vega, dec!(0.0));
assert_eq!(greeks.rho, dec!(0.0));
assert_eq!(greeks.rho_d, dec!(0.0));
assert_eq!(greeks.vanna, dec!(0.0));
assert_eq!(greeks.vomma, dec!(0.0));
assert_eq!(greeks.veta, dec!(0.0));
}
#[test]
fn test_greeks_opposing_positions() {
let option1 = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos_or_panic!(50.0), ExpirationDate::Days(pos_or_panic!(365.0)),
pos_or_panic!(0.2), Positive::ONE,
pos_or_panic!(50.0), dec!(0.05), OptionStyle::Call,
pos_or_panic!(0.01), None, );
let option2 = Options::new(
OptionType::European,
Side::Short,
"TEST".to_string(),
pos_or_panic!(50.0), ExpirationDate::Days(pos_or_panic!(365.0)),
pos_or_panic!(0.2), Positive::ONE,
pos_or_panic!(50.0), dec!(0.05), OptionStyle::Call,
pos_or_panic!(0.01), None, );
let collection = TestOptionCollection {
options: vec![option1, option2],
};
let greeks = collection.greeks().unwrap();
assert_decimal_eq!(greeks.delta, Decimal::ZERO, dec!(0.000001));
assert_decimal_eq!(greeks.gamma, dec!(0.0743013), dec!(0.000001));
assert_decimal_eq!(greeks.vega, dec!(0.37150664), dec!(0.000001));
assert_decimal_eq!(greeks.rho, dec!(0.532324815), dec!(0.000001));
assert_decimal_eq!(greeks.vanna, dec!(-0.55725996), dec!(0.000001));
assert_decimal_eq!(greeks.vomma, dec!(0.09752049), dec!(0.000001));
assert_decimal_eq!(greeks.veta, dec!(0.00000657), dec!(0.000001));
}
#[test]
fn test_individual_greek_methods() {
let option1 = create_test_option(Side::Long, OptionStyle::Call, Positive::ONE);
let option2 = create_test_option(Side::Short, OptionStyle::Put, Positive::ONE);
let collection = TestOptionCollection {
options: vec![option1, option2],
};
let delta = collection.delta().unwrap();
let gamma = collection.gamma().unwrap();
let theta = collection.theta().unwrap();
let vega = collection.vega().unwrap();
let rho = collection.rho().unwrap();
let rho_d = collection.rho_d().unwrap();
let vanna = collection.vanna().unwrap();
let vomma = collection.vomma().unwrap();
let veta = collection.veta().unwrap();
assert!(delta.abs() > dec!(0.0), "Delta calculation failed");
assert!(gamma.abs() > dec!(0.0), "Gamma calculation failed");
assert!(theta.abs() > dec!(0.0), "Theta calculation failed");
assert!(vega.abs() > dec!(0.0), "Vega calculation failed");
assert!(rho.abs() > dec!(0.0), "Rho calculation failed");
assert!(rho_d.abs() > dec!(0.0), "Rho_d calculation failed");
assert!(vanna.abs() > dec!(0.0), "Vanna calculation failed");
assert!(vomma.abs() > dec!(0.0), "Vomma calculation failed");
assert!(veta.abs() > dec!(0.0), "Veta calculation failed");
}
#[test]
fn test_empty_option_collection() {
let collection = TestOptionCollection { options: vec![] };
let greeks = collection.greeks().unwrap();
assert_eq!(greeks.delta, dec!(0.0));
assert_eq!(greeks.gamma, dec!(0.0));
assert_eq!(greeks.theta, dec!(0.0));
assert_eq!(greeks.vega, dec!(0.0));
assert_eq!(greeks.rho, dec!(0.0));
assert_eq!(greeks.rho_d, dec!(0.0));
assert_eq!(greeks.vanna, dec!(0.0));
assert_eq!(greeks.vomma, dec!(0.0));
assert_eq!(greeks.veta, dec!(0.0));
}
#[test]
fn test_greeks_with_different_expirations() {
let mut option1 = create_test_option(Side::Long, OptionStyle::Call, Positive::ONE);
let mut option2 = create_test_option(Side::Long, OptionStyle::Call, Positive::ONE);
option1.expiration_date = ExpirationDate::Days(pos_or_panic!(30.0));
option2.expiration_date = ExpirationDate::Days(pos_or_panic!(60.0));
let collection = TestOptionCollection {
options: vec![option1, option2],
};
let greeks = collection.greeks().unwrap();
assert!(greeks.delta.abs() > dec!(0.0));
assert!(greeks.gamma.abs() > dec!(0.0));
assert!(greeks.theta.abs() > dec!(0.0));
assert!(greeks.vega.abs() > dec!(0.0));
assert!(greeks.rho.abs() > dec!(0.0));
assert!(greeks.rho_d.abs() > dec!(0.0));
assert!(greeks.vanna.abs() > dec!(0.0));
assert!(greeks.vomma.abs() > dec!(0.0));
assert!(greeks.veta.abs() > dec!(0.0));
}
}
#[cfg(test)]
pub mod tests_vanna_equation {
use super::*;
use crate::ExpirationDate;
use crate::model::types::{OptionType, Side};
use num_traits::ToPrimitive;
use positive::constants::DAYS_IN_A_YEAR;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
fn create_test_option(
underlying_price: Positive,
strike_price: Positive,
implied_volatility: Positive,
dividend_yield: Positive,
expiration_in_days: Positive,
) -> Options {
Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
strike_price,
ExpirationDate::Days(expiration_in_days),
implied_volatility,
Positive::ONE, underlying_price,
dec!(0.05), OptionStyle::Call,
dividend_yield,
None, )
}
#[test]
fn test_vanna_atm() {
let option = create_test_option(
Positive::HUNDRED, Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, DAYS_IN_A_YEAR, );
let vanna = vanna(&option).unwrap().to_f64().unwrap();
let expected_vanna = -0.28143026;
assert!(
(vanna - expected_vanna).abs() < 1e-5,
"Vega ATM test failed: expected {expected_vanna}, got {vanna}"
);
}
#[test]
fn test_vanna_otm() {
let option = create_test_option(
pos_or_panic!(90.0), Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, DAYS_IN_A_YEAR, );
let vanna = vanna(&option).unwrap().to_f64().unwrap();
let expected_vanna = 0.73995634;
assert!(
(vanna - expected_vanna).abs() < 1e-5,
"Vanna OTM test failed: expected {expected_vanna}, got {vanna}"
);
}
#[test]
fn test_vanna_short_expiration() {
let option = create_test_option(
Positive::HUNDRED, Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, Positive::ONE, );
let vanna = vanna(&option).unwrap().to_f64().unwrap();
let expected_vanna = -0.01565856;
assert!(
(vanna - expected_vanna).abs() < 1e-5,
"Vanna short expiration test failed: expected {expected_vanna}, got {vanna}"
);
}
#[test]
fn test_vanna_with_dividends() {
let option = create_test_option(
Positive::HUNDRED, Positive::HUNDRED, pos_or_panic!(0.2), pos_or_panic!(0.03), Positive::ONE, );
let vanna = vanna(&option).unwrap().to_f64().unwrap();
let expected_vanna = -0.01565728;
assert!(
(vanna - expected_vanna).abs() < 1e-5,
"Vanna with dividends test failed: expected {expected_vanna}, got {vanna}"
);
}
#[test]
fn test_vanna_itm() {
let option = create_test_option(
pos_or_panic!(110.0), Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, Positive::ONE, );
let vanna = vanna(&option).unwrap().to_f64().unwrap();
let expected_vanna = 0.0;
assert!(
(vanna - expected_vanna).abs() < 1e-5,
"Vanna ITM test failed: expected {expected_vanna}, got {vanna}"
);
}
}
#[cfg(test)]
pub mod tests_vomma_equation {
use super::*;
use crate::ExpirationDate;
use crate::model::types::{OptionType, Side};
use num_traits::ToPrimitive;
use positive::constants::DAYS_IN_A_YEAR;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
fn create_test_option(
underlying_price: Positive,
strike_price: Positive,
implied_volatility: Positive,
dividend_yield: Positive,
expiration_in_days: Positive,
) -> Options {
Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
strike_price,
ExpirationDate::Days(expiration_in_days),
implied_volatility,
Positive::ONE, underlying_price,
dec!(0.05), OptionStyle::Call,
dividend_yield,
None, )
}
#[test]
fn test_vomma_atm() {
let option = create_test_option(
Positive::HUNDRED, Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, DAYS_IN_A_YEAR, );
let vomma = vomma(&option).unwrap().to_f64().unwrap();
let expected_vomma = 0.09850059;
assert!(
(vomma - expected_vomma).abs() < 1e-5,
"Vomma ATM test failed: expected {expected_vomma}, got {vomma}"
);
}
#[test]
fn test_vomma_otm() {
let option = create_test_option(
pos_or_panic!(90.0), Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, DAYS_IN_A_YEAR, );
let vomma = vomma(&option).unwrap().to_f64().unwrap();
let expected_vomma = 0.11774357;
assert!(
(vomma - expected_vomma).abs() < 1e-5,
"Vomma OTM test failed: expected {expected_vomma}, got {vomma}"
);
}
#[test]
fn test_vomma_short_expiration() {
let option = create_test_option(
Positive::HUNDRED, Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, Positive::ONE, );
let vomma = vomma(&option).unwrap().to_f64().unwrap();
let expected_vomma = 0.0000150150;
assert!(
(vomma - expected_vomma).abs() < 1e-5,
"Vomma short expiration test failed: expected {expected_vomma}, got {vomma}"
);
}
#[test]
fn test_vomma_with_dividends() {
let option = create_test_option(
Positive::HUNDRED, Positive::HUNDRED, pos_or_panic!(0.2), pos_or_panic!(0.03), Positive::ONE, );
let vomma = vomma(&option).unwrap().to_f64().unwrap();
let expected_vomma = 0.0000150138;
assert!(
(vomma - expected_vomma).abs() < 1e-5,
"Vomma with dividends test failed: expected {expected_vomma}, got {vomma}"
);
}
#[test]
fn test_vomma_itm() {
let option = create_test_option(
pos_or_panic!(110.0), Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, Positive::ONE, );
let vomma = vomma(&option).unwrap().to_f64().unwrap();
let expected_vomma = 0.0;
assert!(
(vomma - expected_vomma).abs() < 1e-5,
"Vomma ITM test failed: expected {expected_vomma}, got {vomma}"
);
}
}
#[cfg(test)]
pub mod tests_veta_equation {
use super::*;
use crate::ExpirationDate;
use crate::model::types::{OptionType, Side};
use num_traits::ToPrimitive;
use positive::constants::DAYS_IN_A_YEAR;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
fn create_test_option(
underlying_price: Positive,
strike_price: Positive,
implied_volatility: Positive,
dividend_yield: Positive,
expiration_in_days: Positive,
) -> Options {
Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
strike_price,
ExpirationDate::Days(expiration_in_days),
implied_volatility,
Positive::ONE, underlying_price,
dec!(0.05), OptionStyle::Call,
dividend_yield,
None, )
}
#[test]
fn test_veta_atm() {
let option = create_test_option(
Positive::HUNDRED, Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, DAYS_IN_A_YEAR, );
let veta = veta(&option).unwrap().to_f64().unwrap();
let expected_veta = 0.0000065332;
assert!(
(veta - expected_veta).abs() < 1e-5,
"Veta ATM test failed: expected {expected_veta}, got {veta}"
);
}
#[test]
fn test_veta_otm() {
let option = create_test_option(
pos_or_panic!(90.0), Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, DAYS_IN_A_YEAR, );
let veta = veta(&option).unwrap().to_f64().unwrap();
let expected_veta = 0.0000081007;
assert!(
(veta - expected_veta).abs() < 1e-5,
"Veta OTM test failed: expected {expected_veta}, got {veta}"
);
}
#[test]
fn test_veta_short_expiration() {
let option = create_test_option(
Positive::HUNDRED, Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, Positive::ONE, );
let veta = veta(&option).unwrap().to_f64().unwrap();
let expected_veta = 0.0001511497;
assert!(
(veta - expected_veta).abs() < 1e-5,
"Veta short expiration test failed: expected {expected_veta}, got {veta}"
);
}
#[test]
fn test_veta_with_dividends() {
let option = create_test_option(
Positive::HUNDRED, Positive::HUNDRED, pos_or_panic!(0.2), pos_or_panic!(0.03), Positive::ONE, );
let veta = veta(&option).unwrap().to_f64().unwrap();
let expected_veta = 0.0001511559;
assert!(
(veta - expected_veta).abs() < 1e-5,
"Veta with dividends test failed: expected {expected_veta}, got {veta}"
);
}
#[test]
fn test_veta_itm() {
let option = create_test_option(
pos_or_panic!(110.0), Positive::HUNDRED, pos_or_panic!(0.2), Positive::ZERO, Positive::ONE, );
let veta = veta(&option).unwrap().to_f64().unwrap();
let expected_veta = 0.0;
assert!(
(veta - expected_veta).abs() < 1e-5,
"Veta ITM test failed: expected {expected_veta}, got {veta}"
);
}
}
#[cfg(test)]
pub mod tests_charm_equations {
use super::*;
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option_with_days;
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use positive::pos_or_panic;
use tracing::info;
#[test]
fn test_charm_call_itm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.3), pos_or_panic!(30.0), );
let charm_value = charm(&option).unwrap();
info!("Charm Call ITM Value: {}", charm_value);
assert_relative_eq!(charm_value.to_f64().unwrap(), 0.00277350, epsilon = 1e-8);
}
#[test]
fn test_charm_put_itm() {
let option = create_sample_option_with_days(
OptionStyle::Put,
Side::Long,
pos_or_panic!(95.0), Positive::ONE, Positive::HUNDRED, pos_or_panic!(0.3), pos_or_panic!(30.0), );
let charm_value = charm(&option).unwrap();
info!("Charm Put ITM Value: {}", charm_value);
assert_relative_eq!(charm_value.to_f64().unwrap(), -0.00392474, epsilon = 1e-8);
}
#[test]
fn test_charm_call_atm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(95.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.3), pos_or_panic!(30.0), );
let charm_value = charm(&option).unwrap();
info!("Charm Call ATM Value: {}", charm_value);
assert_relative_eq!(charm_value.to_f64().unwrap(), -0.00045952, epsilon = 1e-8);
}
#[test]
fn test_charm_put_atm() {
let option = create_sample_option_with_days(
OptionStyle::Put,
Side::Long,
pos_or_panic!(95.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.3), pos_or_panic!(30.0), );
let charm_value = charm(&option).unwrap();
info!("Charm Put ATM Value: {}", charm_value);
assert_relative_eq!(charm_value.to_f64().unwrap(), -0.00048690, epsilon = 1e-8);
}
#[test]
fn test_charm_call_otm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(90.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.3), pos_or_panic!(30.0), );
let charm_value = charm(&option).unwrap();
info!("Charm Call OTM Value: {}", charm_value);
assert_relative_eq!(charm_value.to_f64().unwrap(), -0.00401791, epsilon = 1e-8);
}
#[test]
fn test_charm_put_otm() {
let option = create_sample_option_with_days(
OptionStyle::Put,
Side::Long,
pos_or_panic!(95.0), Positive::ONE, pos_or_panic!(90.0), pos_or_panic!(0.3), pos_or_panic!(30.0), );
let charm_value = charm(&option).unwrap();
info!("Charm Put OTM Value: {}", charm_value);
assert_relative_eq!(charm_value.to_f64().unwrap(), 0.00285007, epsilon = 1e-8);
}
}
#[cfg(test)]
pub mod tests_volatility_greeks_edge_cases {
use super::*;
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option_with_days;
use positive::pos_or_panic;
use tracing::info;
#[test]
fn test_vanna_high_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.8), pos_or_panic!(30.0),
);
let vanna_value = vanna(&option).unwrap();
info!("Vanna High Volatility: {}", vanna_value);
assert!(vanna_value.abs() < Decimal::ONE);
}
#[test]
fn test_vanna_low_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.05), pos_or_panic!(30.0),
);
let vanna_value = vanna(&option).unwrap();
info!("Vanna Low Volatility: {}", vanna_value);
assert!(vanna_value.abs() < Decimal::MAX);
}
#[test]
fn test_vanna_near_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
Positive::ONE, );
let vanna_value = vanna(&option).unwrap();
info!("Vanna Near Expiration: {}", vanna_value);
assert!(vanna_value.abs() < Decimal::MAX);
}
#[test]
fn test_vanna_deep_itm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0),
);
let vanna_value = vanna(&option).unwrap();
info!("Vanna Deep ITM: {}", vanna_value);
assert!(vanna_value.abs() < Decimal::ONE);
}
#[test]
fn test_vanna_deep_otm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(50.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0),
);
let vanna_value = vanna(&option).unwrap();
info!("Vanna Deep OTM: {}", vanna_value);
assert!(vanna_value.abs() < Decimal::ONE);
}
#[test]
fn test_vanna_zero_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
Positive::ZERO, pos_or_panic!(30.0),
);
let vanna_value = vanna(&option).unwrap();
info!("Vanna Zero Volatility: {}", vanna_value);
assert_eq!(vanna_value, Decimal::ZERO);
}
#[test]
fn test_vomma_high_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.8), pos_or_panic!(30.0),
);
let vomma_value = vomma(&option).unwrap();
info!("Vomma High Volatility: {}", vomma_value);
assert!(vomma_value.abs() < Decimal::MAX);
}
#[test]
fn test_vomma_low_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.05), pos_or_panic!(30.0),
);
let vomma_value = vomma(&option).unwrap();
info!("Vomma Low Volatility: {}", vomma_value);
assert!(vomma_value.abs() < Decimal::MAX);
}
#[test]
fn test_vomma_near_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
Positive::ONE, );
let vomma_value = vomma(&option).unwrap();
info!("Vomma Near Expiration: {}", vomma_value);
assert!(vomma_value.abs() < Decimal::MAX);
}
#[test]
fn test_vomma_at_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
Positive::ZERO, );
let vomma_value = vomma(&option).unwrap();
info!("Vomma At Expiration: {}", vomma_value);
assert_eq!(vomma_value, Decimal::ZERO);
}
#[test]
fn test_vomma_deep_otm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(50.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0),
);
let vomma_value = vomma(&option).unwrap();
info!("Vomma Deep OTM: {}", vomma_value);
assert!(vomma_value.abs() < Decimal::MAX);
}
#[test]
fn test_veta_high_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.8), pos_or_panic!(30.0),
);
let veta_value = veta(&option).unwrap();
info!("Veta High Volatility: {}", veta_value);
assert!(veta_value.abs() < Decimal::MAX);
}
#[test]
fn test_veta_low_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.05), pos_or_panic!(30.0),
);
let veta_value = veta(&option).unwrap();
info!("Veta Low Volatility: {}", veta_value);
assert!(veta_value.abs() < Decimal::MAX);
}
#[test]
fn test_veta_near_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
Positive::ONE, );
let veta_value = veta(&option).unwrap();
info!("Veta Near Expiration: {}", veta_value);
assert!(veta_value.abs() < Decimal::MAX);
}
#[test]
fn test_veta_at_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
Positive::ZERO, );
let veta_value = veta(&option).unwrap();
info!("Veta At Expiration: {}", veta_value);
assert_eq!(veta_value, Decimal::ZERO);
}
#[test]
fn test_veta_deep_itm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0),
);
let veta_value = veta(&option).unwrap();
info!("Veta Deep ITM: {}", veta_value);
assert!(veta_value.abs() < Decimal::MAX);
}
#[test]
fn test_veta_deep_otm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(50.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0),
);
let veta_value = veta(&option).unwrap();
info!("Veta Deep OTM: {}", veta_value);
assert!(veta_value.abs() < Decimal::MAX);
}
#[test]
fn test_veta_long_dated_option() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(365.0), );
let veta_value = veta(&option).unwrap();
info!("Veta Long Dated: {}", veta_value);
assert!(veta_value.abs() < Decimal::MAX);
}
#[test]
fn test_charm_high_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE,
Positive::HUNDRED, pos_or_panic!(0.8), pos_or_panic!(30.0), );
let charm_value = charm(&option).unwrap();
info!("Charm High Volatility: {}", charm_value);
assert!(charm_value.abs() < Decimal::ONE);
}
#[test]
fn test_charm_low_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE,
Positive::HUNDRED, pos_or_panic!(0.05), pos_or_panic!(30.0), );
let charm_value = charm(&option).unwrap();
info!("Charm Low Volatility: {}", charm_value);
assert!(charm_value.abs() < Decimal::ONE);
}
#[test]
fn test_charm_near_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE,
Positive::HUNDRED, pos_or_panic!(0.2),
Positive::ONE, );
let charm_value = charm(&option).unwrap();
info!("Charm Near Expiration: {}", charm_value);
assert!(charm_value.abs() < Decimal::ONE);
}
#[test]
fn test_charm_at_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE,
Positive::HUNDRED, pos_or_panic!(0.2),
Positive::ZERO, );
let charm_value = charm(&option).unwrap();
info!("Charm At Expiration: {}", charm_value);
assert_eq!(charm_value, Decimal::ZERO);
}
#[test]
fn test_charm_deep_itm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0), );
let charm_value = charm(&option).unwrap();
info!("Charm Deep ITM: {}", charm_value);
assert!(charm_value.abs() < Decimal::ONE);
}
#[test]
fn test_charm_deep_otm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(50.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0), );
let charm_value = charm(&option).unwrap();
info!("Charm Deep OTM: {}", charm_value);
assert_eq!(charm_value.abs(), Decimal::ZERO);
}
#[test]
fn test_charm_long_dated_option() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(365.0), );
let charm_value = charm(&option).unwrap();
info!("Charm Long Dated: {}", charm_value);
assert!(charm_value.abs() < Decimal::ONE);
}
#[test]
fn test_color_high_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE,
Positive::HUNDRED, pos_or_panic!(0.8), pos_or_panic!(30.0), );
let color_value = color(&option).unwrap();
info!("Color High Volatility: {}", color_value);
assert!(color_value.abs() < Decimal::ONE);
}
#[test]
fn test_color_low_volatility() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE,
Positive::HUNDRED, pos_or_panic!(0.05), pos_or_panic!(30.0), );
let color_value = color(&option).unwrap();
info!("Color Low Volatility: {}", color_value);
assert!(color_value.abs() < Decimal::ONE);
}
#[test]
fn test_color_near_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE,
Positive::HUNDRED, pos_or_panic!(0.2),
Positive::ONE, );
let color_value = color(&option).unwrap();
info!("Color Near Expiration: {}", color_value);
assert!(color_value.abs() < Decimal::ONE);
}
#[test]
fn test_color_at_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE,
Positive::HUNDRED, pos_or_panic!(0.2),
Positive::ZERO, );
let color_value = color(&option).unwrap();
info!("Color At Expiration: {}", color_value);
assert_eq!(color_value, Decimal::ZERO);
}
#[test]
fn test_color_deep_itm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(150.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0), );
let color_value = color(&option).unwrap();
info!("Color Deep ITM: {}", color_value);
assert!(color_value.abs() < Decimal::ONE);
}
#[test]
fn test_color_deep_otm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(50.0), Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0), );
let color_value = color(&option).unwrap();
info!("Color Deep OTM: {}", color_value);
assert_eq!(color_value.abs(), Decimal::ZERO);
}
#[test]
fn test_color_long_dated_option() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(365.0), );
let color_value = color(&option).unwrap();
info!("Color Long Dated: {}", color_value);
assert!(color_value.abs() < Decimal::ONE);
}
#[test]
fn test_volatility_greeks_extreme_scenario() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
Positive::ONE, Positive::TWO, );
let vanna_value = vanna(&option).unwrap();
let vomma_value = vomma(&option).unwrap();
let veta_value = veta(&option).unwrap();
let charm_value = charm(&option).unwrap();
let color_value = color(&option).unwrap();
info!("Extreme Scenario - Vanna: {}", vanna_value);
info!("Extreme Scenario - Vomma: {}", vomma_value);
info!("Extreme Scenario - Veta: {}", veta_value);
info!("Extreme Scenario - Charm: {}", charm_value);
info!("Extreme Scenario - Color: {}", color_value);
assert!(vanna_value.abs() < Decimal::MAX);
assert!(vomma_value.abs() < Decimal::MAX);
assert!(veta_value.abs() < Decimal::MAX);
assert!(charm_value.abs() < Decimal::MAX);
assert!(color_value.abs() < Decimal::MAX);
}
#[test]
fn test_volatility_greeks_put_option() {
let option = create_sample_option_with_days(
OptionStyle::Put,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED,
pos_or_panic!(0.2),
pos_or_panic!(30.0),
);
let vanna_value = vanna(&option).unwrap();
let vomma_value = vomma(&option).unwrap();
let veta_value = veta(&option).unwrap();
let charm_value = charm(&option).unwrap();
let color_value = color(&option).unwrap();
info!("Put Option - Vanna: {}", vanna_value);
info!("Put Option - Vomma: {}", vomma_value);
info!("Put Option - Veta: {}", veta_value);
info!("Put Option - Charm: {}", charm_value);
info!("Put Option - Color: {}", color_value);
assert!(vanna_value.abs() < Decimal::MAX);
assert!(vomma_value.abs() < Decimal::MAX);
assert!(veta_value.abs() < Decimal::MAX);
assert!(charm_value.abs() < Decimal::MAX);
assert!(color_value.abs() < Decimal::MAX);
}
#[test]
fn test_vanna_atm_vs_otm_comparison() {
let atm_option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
Positive::HUNDRED, pos_or_panic!(0.2),
pos_or_panic!(30.0),
);
let otm_option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
pos_or_panic!(110.0), pos_or_panic!(0.2),
pos_or_panic!(30.0),
);
let vanna_atm = vanna(&atm_option).unwrap();
let vanna_otm = vanna(&otm_option).unwrap();
info!("Vanna ATM: {}", vanna_atm);
info!("Vanna OTM: {}", vanna_otm);
assert!(vanna_atm.abs() < Decimal::MAX);
assert!(vanna_otm.abs() < Decimal::MAX);
}
#[test]
fn test_vomma_smile_effect() {
let strikes = vec![
pos_or_panic!(90.0),
pos_or_panic!(95.0),
Positive::HUNDRED,
pos_or_panic!(105.0),
pos_or_panic!(110.0),
];
for strike in strikes {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED,
Positive::ONE,
strike,
pos_or_panic!(0.2),
pos_or_panic!(30.0),
);
let vomma_value = vomma(&option).unwrap();
info!("Vomma at strike {}: {}", strike, vomma_value);
assert!(vomma_value.abs() < Decimal::MAX);
}
}
}
#[cfg(test)]
pub mod tests_color_equations {
use super::*;
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option_with_days;
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use positive::pos_or_panic;
use tracing::info;
#[test]
fn test_color_itm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
Positive::HUNDRED, Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.3), pos_or_panic!(30.0), );
let color_value = color(&option).unwrap();
info!("Color ITM Value: {}", color_value);
assert_relative_eq!(color_value.to_f64().unwrap(), -0.00039105, epsilon = 1e-8);
}
#[test]
fn test_color_atm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(95.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.3), pos_or_panic!(30.0), );
let color_value = color(&option).unwrap();
info!("Color ATM Value: {}", color_value);
assert_relative_eq!(color_value.to_f64().unwrap(), -0.00081635, epsilon = 1e-8);
}
#[test]
fn test_color_atm_near_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(95.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.3), pos_or_panic!(0.5), );
let color_value = color(&option).unwrap();
info!("Color ATM Near Expiration Value: {}", color_value);
assert_relative_eq!(color_value.to_f64().unwrap(), -0.37822466, epsilon = 1e-8);
}
#[test]
fn test_color_atm_right_before_expiration() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(95.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.3), pos_or_panic!(0.001), );
let color_value = color(&option).unwrap();
info!("Color ATM Right Before Expiration Value: {}", color_value);
assert_relative_eq!(
color_value.to_f64().unwrap(),
-4228.45476344,
epsilon = 1e-8
);
}
#[test]
fn test_color_otm() {
let option = create_sample_option_with_days(
OptionStyle::Call,
Side::Long,
pos_or_panic!(90.0), Positive::ONE, pos_or_panic!(95.0), pos_or_panic!(0.3), pos_or_panic!(30.0), );
let color_value = color(&option).unwrap();
info!("Color OTM Value: {}", color_value);
assert_relative_eq!(color_value.to_f64().unwrap(), -0.00046416, epsilon = 1e-8);
}
}