use crate::constants::ZERO;
use crate::error::greeks::GreeksError;
use crate::greeks::utils::{big_n, d1, d2, n};
use crate::model::types::OptionStyle;
use crate::{Options, Positive, Side};
use rust_decimal::{Decimal, MathematicalOps};
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq)]
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,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
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 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()?;
Ok(Greek {
delta,
gamma,
theta,
vega,
rho,
rho_d,
alpha,
})
}
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)
}
}
pub fn delta(option: &Options) -> Result<Decimal, GreeksError> {
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,
option.expiration_date.get_years().unwrap(),
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 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,
option.expiration_date.get_years().unwrap(),
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,
option.expiration_date.get_years().unwrap(),
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 d1 = d1(
option.underlying_price,
option.strike_price,
option.risk_free_rate,
option.expiration_date.get_years().unwrap(),
option.implied_volatility,
)?;
let expiration_date: Positive = option.expiration_date.get_years()?;
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),
}
}
#[cfg(test)]
pub mod tests_delta_equations {
use super::*;
use crate::constants::{DAYS_IN_A_YEAR, ZERO};
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option;
use crate::strategies::DELTA_THRESHOLD;
use crate::{ExpirationDate, assert_decimal_eq, pos};
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
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!(150.0),
pos!(1.0),
pos!(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!(110.0),
pos!(1.0),
pos!(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!(150.0),
pos!(1.0),
pos!(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!(160.0),
pos!(1.0),
pos!(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!(150.0),
pos!(1.0),
pos!(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!(110.0),
pos!(1.0),
pos!(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!(150.0),
pos!(1.0),
pos!(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!(160.0),
pos!(1.0),
pos!(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!(150.0),
pos!(1.0),
pos!(100.0),
pos!(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!(50.0),
pos!(1.0),
pos!(100.0),
pos!(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,
pos!(100.0),
pos!(1.0),
pos!(100.0),
pos!(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,
pos!(100.0),
pos!(1.0),
pos!(100.0),
pos!(0.50),
);
option.expiration_date = ExpirationDate::Days(pos!(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,
pos!(100.0),
pos!(1.0),
pos!(100.0),
pos!(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!(21637.0),
pos!(1.0),
pos!(21825.0),
pos!(0.219),
);
option.expiration_date = ExpirationDate::Days(pos!(1.0));
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::constants::DAYS_IN_A_YEAR;
use crate::model::types::{OptionStyle, Side};
use crate::model::utils::create_sample_option;
use crate::{ExpirationDate, pos};
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use tracing::info;
#[test]
fn test_gamma_deep_in_the_money_call() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos!(150.0),
pos!(1.0),
pos!(120.0),
pos!(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!(50.0),
pos!(1.0),
pos!(100.0),
pos!(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,
pos!(100.0),
pos!(1.0),
pos!(100.0),
pos!(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,
pos!(100.0),
pos!(1.0),
pos!(100.0),
pos!(0.50),
);
option.expiration_date = ExpirationDate::Days(pos!(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,
pos!(100.0),
pos!(1.0),
pos!(100.0),
pos!(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,
pos!(100.0),
pos!(1.0),
pos!(100.0),
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,
pos!(100.0),
pos!(1.0),
pos!(100.0),
pos!(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, pos};
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
use tracing::info;
#[test]
fn test_50_vol_10() {
let option = Options::new(
OptionType::European,
Side::Long,
"XYZ".parse().unwrap(),
pos!(50.0),
ExpirationDate::Days(pos!(365.0)),
pos!(0.10),
pos!(1.0),
pos!(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!(50.0),
ExpirationDate::Days(pos!(365.0)),
pos!(0.05),
pos!(1.0),
pos!(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!(50.0),
ExpirationDate::Days(pos!(365.0)),
pos!(0.2),
pos!(1.0),
pos!(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::constants::DAYS_IN_A_YEAR;
use crate::model::types::{OptionType, Side};
use crate::{ExpirationDate, Positive, pos};
use num_traits::ToPrimitive;
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,
pos!(1.0), underlying_price,
dec!(0.05), OptionStyle::Call,
dividend_yield,
None, )
}
#[test]
fn test_vega_atm() {
let option = create_test_option(
pos!(100.0),
pos!(100.0),
pos!(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!(90.0),
pos!(100.0),
pos!(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(
pos!(100.0),
pos!(100.0),
pos!(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(
pos!(100.0),
pos!(100.0),
pos!(0.2),
pos!(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!(110.0),
pos!(100.0),
pos!(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::constants::DAYS_IN_A_YEAR;
use crate::model::types::{OptionStyle, OptionType, Side};
use crate::{ExpirationDate, assert_decimal_eq, pos};
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
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: pos!(100.0),
expiration_date: ExpirationDate::Days(DAYS_IN_A_YEAR),
implied_volatility: pos!(0.2),
quantity: pos!(1.0),
underlying_price: pos!(100.0),
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!(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 = pos!(1.0);
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::constants::DAYS_IN_A_YEAR;
use crate::model::types::Side;
use crate::model::utils::create_sample_option;
use crate::{ExpirationDate, pos};
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
#[test]
fn test_theta_call_option() {
let option = create_sample_option(
OptionStyle::Call,
Side::Long,
pos!(150.0), pos!(1.0), pos!(155.0), pos!(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!(150.0), pos!(1.0), pos!(145.0), pos!(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!(150.0), pos!(1.0), pos!(150.0), pos!(0.15), );
option.expiration_date = ExpirationDate::Days(pos!(1.0));
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!(140.0), pos!(1.0), pos!(130.0), pos!(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::constants::DAYS_IN_A_YEAR;
use crate::model::types::Side;
use crate::model::utils::create_sample_option;
use crate::{ExpirationDate, pos};
use approx::assert_relative_eq;
use num_traits::ToPrimitive;
#[test]
fn test_theta_short_call_option() {
let option = create_sample_option(
OptionStyle::Call,
Side::Short,
pos!(150.0), pos!(1.0), pos!(155.0), pos!(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!(150.0), pos!(1.0), pos!(145.0), pos!(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!(150.0), pos!(1.0), pos!(150.0), pos!(0.15), );
option.expiration_date = ExpirationDate::Days(pos!(1.0));
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!(140.0), pos!(1.0), pos!(130.0), pos!(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, pos};
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(),
pos!(100.0), ExpirationDate::Days(pos!(30.0)),
pos!(0.2), quantity,
pos!(100.0), dec!(0.05), style,
pos!(0.01), None, )
}
#[test]
fn test_greeks_single_option() {
let option = create_test_option(Side::Long, OptionStyle::Call, pos!(1.0));
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));
}
#[test]
fn test_greeks_multiple_options() {
let option1 = create_test_option(Side::Long, OptionStyle::Call, pos!(1.0));
let option2 = create_test_option(Side::Short, OptionStyle::Put, pos!(1.0));
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"
);
}
#[test]
fn test_greeks_simple_validation() {
let option = Options::new(
OptionType::European,
Side::Long,
"AAPL".to_string(),
pos!(155.0),
ExpirationDate::Days(pos!(30.0)),
pos!(0.20),
pos!(1.0),
pos!(150.0),
dec!(0.05),
OptionStyle::Call,
pos!(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));
}
#[test]
fn test_greeks_zero_quantity() {
let option = create_test_option(Side::Long, OptionStyle::Call, pos!(0.0));
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));
}
#[test]
fn test_greeks_opposing_positions() {
let option1 = Options::new(
OptionType::European,
Side::Long,
"TEST".to_string(),
pos!(50.0), ExpirationDate::Days(pos!(365.0)),
pos!(0.2), Positive::ONE,
pos!(50.0), dec!(0.05), OptionStyle::Call,
pos!(0.01), None, );
let option2 = Options::new(
OptionType::European,
Side::Short,
"TEST".to_string(),
pos!(50.0), ExpirationDate::Days(pos!(365.0)),
pos!(0.2), Positive::ONE,
pos!(50.0), dec!(0.05), OptionStyle::Call,
pos!(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));
}
#[test]
fn test_individual_greek_methods() {
let option1 = create_test_option(Side::Long, OptionStyle::Call, pos!(1.0));
let option2 = create_test_option(Side::Short, OptionStyle::Put, pos!(1.0));
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();
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");
}
#[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));
}
#[test]
fn test_greeks_with_different_expirations() {
let mut option1 = create_test_option(Side::Long, OptionStyle::Call, pos!(1.0));
let mut option2 = create_test_option(Side::Long, OptionStyle::Call, pos!(1.0));
option1.expiration_date = ExpirationDate::Days(pos!(30.0));
option2.expiration_date = ExpirationDate::Days(pos!(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));
}
}