use crate::constants::{TRADING_DAYS, ZERO};
use crate::error::greeks::{CalculationErrorKind, GreeksError};
use crate::greeks::utils::{big_n, d1, n};
use crate::model::decimal::{d_add, d_div, d_exp, d_mul, d_sub};
use crate::model::types::{OptionStyle, OptionType};
use crate::{Options, Side};
use positive::Positive;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::cell::OnceCell;
use utoipa::ToSchema;
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, ToSchema)]
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,
}
impl From<Greek> for GreeksSnapshot {
#[inline]
fn from(greek: Greek) -> Self {
Self {
delta: greek.delta,
gamma: greek.gamma,
theta: greek.theta,
vega: greek.vega,
rho: Some(greek.rho),
rho_d: Some(greek.rho_d),
alpha: Some(greek.alpha),
vanna: greek.vanna,
vomma: greek.vomma,
veta: greek.veta,
charm: greek.charm,
color: greek.color,
}
}
}
pub trait Greeks {
fn get_options(&self) -> Result<Vec<&Options>, GreeksError>;
fn greeks(&self) -> Result<Greek, GreeksError> {
let options = self.get_options()?;
let mut delta = Decimal::ZERO;
let mut gamma = Decimal::ZERO;
let mut theta = Decimal::ZERO;
let mut vega = Decimal::ZERO;
let mut rho = Decimal::ZERO;
let mut rho_d = Decimal::ZERO;
let mut alpha = AlphaSum::default();
let mut vanna = Decimal::ZERO;
let mut vomma = Decimal::ZERO;
let mut veta = Decimal::ZERO;
let mut charm = Decimal::ZERO;
let mut color = Decimal::ZERO;
for (index, option) in options.into_iter().enumerate() {
let single = greeks_for(option)?;
delta = d_add(delta, single.delta, "greeks::aggregate::delta")?;
gamma = d_add(gamma, single.gamma, "greeks::aggregate::gamma")?;
theta = d_add(theta, single.theta, "greeks::aggregate::theta")?;
vega = d_add(vega, single.vega, "greeks::aggregate::vega")?;
rho = d_add(rho, single.rho, "greeks::aggregate::rho")?;
rho_d = d_add(rho_d, single.rho_d, "greeks::aggregate::rho_d")?;
alpha = alpha.push(single.alpha, option, index, "greeks::aggregate::alpha")?;
vanna = d_add(vanna, single.vanna, "greeks::aggregate::vanna")?;
vomma = d_add(vomma, single.vomma, "greeks::aggregate::vomma")?;
veta = d_add(veta, single.veta, "greeks::aggregate::veta")?;
charm = d_add(charm, single.charm, "greeks::aggregate::charm")?;
color = d_add(color, single.color, "greeks::aggregate::color")?;
}
Ok(Greek {
delta,
gamma,
theta,
vega,
rho,
rho_d,
alpha: alpha.total,
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 = d_add(delta_value, delta(option)?, "greeks::delta::aggregate")?;
}
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 = d_add(gamma_value, gamma(option)?, "greeks::gamma::aggregate")?;
}
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 = d_add(theta_value, theta(option)?, "greeks::theta::aggregate")?;
}
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 = d_add(vega_value, vega(option)?, "greeks::vega::aggregate")?;
}
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 = d_add(rho_value, rho(option)?, "greeks::rho::aggregate")?;
}
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 = d_add(rho_d_value, rho_d(option)?, "greeks::rho_d::aggregate")?;
}
Ok(rho_d_value)
}
fn alpha(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut alpha_value = AlphaSum::default();
for (index, option) in options.into_iter().enumerate() {
alpha_value =
alpha_value.push(alpha(option)?, option, index, "greeks::alpha::aggregate")?;
}
Ok(alpha_value.total)
}
fn vanna(&self) -> Result<Decimal, GreeksError> {
let options = self.get_options()?;
let mut vanna_value = Decimal::ZERO;
for option in options {
vanna_value = d_add(vanna_value, vanna(option)?, "greeks::vanna::aggregate")?;
}
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 = d_add(vomma_value, vomma(option)?, "greeks::vomma::aggregate")?;
}
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 = d_add(veta_value, veta(option)?, "greeks::veta::aggregate")?;
}
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 = d_add(charm_value, charm(option)?, "greeks::charm::aggregate")?;
}
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 = d_add(color_value, color(option)?, "greeks::color::aggregate")?;
}
Ok(color_value)
}
}
#[derive(Debug, Clone)]
pub(crate) struct BlackScholesKernels {
t: Positive,
sqrt_t: Positive,
d1: Decimal,
sigma: Positive,
q: Decimal,
r: Decimal,
d2: OnceCell<Decimal>,
n_d1: OnceCell<Decimal>,
big_n_d1: OnceCell<Decimal>,
big_n_neg_d1: OnceCell<Decimal>,
big_n_d2: OnceCell<Decimal>,
big_n_neg_d2: OnceCell<Decimal>,
exp_minus_qt: OnceCell<Decimal>,
exp_minus_rt: OnceCell<Decimal>,
}
impl BlackScholesKernels {
fn new(option: &Options, t: Positive) -> Result<Self, GreeksError> {
let carry_rate = d_sub(
option.risk_free_rate,
option.dividend_yield.to_dec(),
"greeks::kernels::carry_rate",
)?;
let sqrt_t = t.checked_sqrt()?;
let d1 = d1(
option.underlying_price,
option.strike_price,
carry_rate,
t,
option.implied_volatility,
)?;
Ok(Self {
t,
sqrt_t,
d1,
sigma: option.implied_volatility,
q: option.dividend_yield.to_dec(),
r: option.risk_free_rate,
d2: OnceCell::new(),
n_d1: OnceCell::new(),
big_n_d1: OnceCell::new(),
big_n_neg_d1: OnceCell::new(),
big_n_d2: OnceCell::new(),
big_n_neg_d2: OnceCell::new(),
exp_minus_qt: OnceCell::new(),
exp_minus_rt: OnceCell::new(),
})
}
fn t(&self) -> Positive {
self.t
}
fn sqrt_t(&self) -> Positive {
self.sqrt_t
}
fn d1(&self) -> Decimal {
self.d1
}
fn d2(&self) -> Result<Decimal, GreeksError> {
cached(&self.d2, || {
let vol_time = d_mul(
self.sigma.to_dec(),
self.sqrt_t.to_dec(),
"greeks::kernels::d2::vol_time",
)?;
Ok(d_sub(self.d1, vol_time, "greeks::kernels::d2")?)
})
}
fn n_d1(&self) -> Result<Decimal, GreeksError> {
cached(&self.n_d1, || n(self.d1))
}
fn big_n_d1(&self) -> Result<Decimal, GreeksError> {
cached(&self.big_n_d1, || Ok(big_n(self.d1)?))
}
fn big_n_neg_d1(&self) -> Result<Decimal, GreeksError> {
cached(&self.big_n_neg_d1, || Ok(big_n(-self.d1)?))
}
fn big_n_d2(&self) -> Result<Decimal, GreeksError> {
cached(&self.big_n_d2, || Ok(big_n(self.d2()?)?))
}
fn big_n_neg_d2(&self) -> Result<Decimal, GreeksError> {
cached(&self.big_n_neg_d2, || Ok(big_n(-self.d2()?)?))
}
fn exp_minus_qt(&self) -> Result<Decimal, GreeksError> {
cached(&self.exp_minus_qt, || {
let exponent = d_mul(
-self.t.to_dec(),
self.q,
"greeks::kernels::exp_minus_qt::exponent",
)?;
Ok(d_exp(exponent, "greeks::kernels::exp_minus_qt")?)
})
}
fn exp_minus_rt(&self) -> Result<Decimal, GreeksError> {
cached(&self.exp_minus_rt, || {
let exponent = d_mul(
-self.r,
self.t.to_dec(),
"greeks::kernels::exp_minus_rt::exponent",
)?;
Ok(d_exp(exponent, "greeks::kernels::exp_minus_rt")?)
})
}
}
fn cached<F>(cell: &OnceCell<Decimal>, compute: F) -> Result<Decimal, GreeksError>
where
F: FnOnce() -> Result<Decimal, GreeksError>,
{
if let Some(value) = cell.get() {
return Ok(*value);
}
let value = compute()?;
let _ = cell.set(value);
Ok(value)
}
#[inline]
#[must_use]
fn signed_quantity(option: &Options) -> Decimal {
let quantity = option.quantity.to_dec();
if option.is_long() {
quantity
} else {
-quantity
}
}
fn kernels_for(option: &Options) -> Result<Option<BlackScholesKernels>, GreeksError> {
if !matches!(option.option_type, OptionType::European) {
return Ok(None);
}
let t = option.expiration_date.get_years()?;
if t == Decimal::ZERO || option.implied_volatility == ZERO {
return Ok(None);
}
Ok(Some(BlackScholesKernels::new(option, t)?))
}
fn greeks_for(option: &Options) -> Result<Greek, GreeksError> {
let Some(kernels) = kernels_for(option)? else {
let gamma = gamma(option)?;
let theta = theta(option)?;
return Ok(Greek {
delta: delta(option)?,
gamma,
theta,
vega: vega(option)?,
rho: rho(option)?,
rho_d: rho_d(option)?,
alpha: alpha_from(gamma, theta)?,
vanna: vanna(option)?,
vomma: vomma(option)?,
veta: veta(option)?,
charm: charm(option)?,
color: color(option)?,
});
};
let gamma = gamma_with(option, &kernels)?;
let theta = theta_with(option, &kernels)?;
Ok(Greek {
delta: delta_with(option, &kernels)?,
gamma,
theta,
vega: vega_with(option, &kernels)?,
rho: rho_with(option, &kernels)?,
rho_d: rho_d_with(option, &kernels)?,
alpha: alpha_from(gamma, theta)?,
vanna: vanna_with(option, &kernels)?,
vomma: vomma_with(option, &kernels)?,
veta: veta_with(option, &kernels)?,
charm: charm_with(option, &kernels)?,
color: color_with(option, &kernels)?,
})
}
pub fn delta(option: &Options) -> Result<Decimal, GreeksError> {
if !matches!(option.option_type, OptionType::European) {
return Ok(d_mul(
crate::greeks::numerical::numerical_delta(option)?,
signed_quantity(option),
"greeks::delta::numerical_position_weighted",
)?);
}
let expiration_date = option.expiration_date.get_years()?;
if expiration_date == Decimal::ZERO {
let per_contract = match (
&option.option_style,
&option.side,
&option.strike_price,
&option.underlying_price,
) {
(OptionStyle::Call, Side::Long, strike, price) if price > strike => Decimal::ONE,
(OptionStyle::Call, Side::Long, _, _) => Decimal::ZERO,
(OptionStyle::Call, Side::Short, strike, price) if price > strike => -Decimal::ONE,
(OptionStyle::Call, Side::Short, _, _) => Decimal::ZERO,
(OptionStyle::Put, Side::Long, strike, price) if price < strike => -Decimal::ONE,
(OptionStyle::Put, Side::Long, _, _) => Decimal::ZERO,
(OptionStyle::Put, Side::Short, strike, price) if price < strike => Decimal::ONE,
(OptionStyle::Put, Side::Short, _, _) => Decimal::ZERO,
};
return Ok(per_contract * option.quantity.to_dec());
}
let sign = if option.is_long() {
Decimal::ONE
} else {
Decimal::NEGATIVE_ONE
};
if option.implied_volatility == ZERO {
let per_contract = match option.option_style {
OptionStyle::Call => {
if option.underlying_price >= option.strike_price {
sign } else {
Decimal::ZERO }
}
OptionStyle::Put => {
if option.underlying_price <= option.strike_price {
sign * Decimal::NEGATIVE_ONE } else {
Decimal::ZERO }
}
};
return Ok(per_contract * option.quantity.to_dec());
}
let kernels = BlackScholesKernels::new(option, expiration_date)?;
delta_with(option, &kernels)
}
fn delta_with(option: &Options, k: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
let sign = if option.is_long() {
Decimal::ONE
} else {
Decimal::NEGATIVE_ONE
};
let div_date = k.exp_minus_qt()?;
let n_d1 = k.big_n_d1()?;
let delta = match option.option_style {
OptionStyle::Call => d_mul(sign, n_d1, "greeks::delta::call_sign")?,
OptionStyle::Put => d_mul(
sign,
d_sub(n_d1, Decimal::ONE, "greeks::delta::put_shift")?,
"greeks::delta::put_sign",
)?,
};
let delta = d_mul(delta, div_date, "greeks::delta::discounted")?;
let delta: Decimal = delta.clamp(Decimal::NEGATIVE_ONE, Decimal::ONE);
let quantity: Decimal = option.quantity.into();
Ok(d_mul(delta, quantity, "greeks::delta::position_weighted")?)
}
pub fn gamma(option: &Options) -> Result<Decimal, GreeksError> {
if !matches!(option.option_type, OptionType::European) {
return Ok(d_mul(
crate::greeks::numerical::numerical_gamma(option)?,
signed_quantity(option),
"greeks::gamma::numerical_position_weighted",
)?);
}
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 kernels = BlackScholesKernels::new(option, expiration_date)?;
gamma_with(option, &kernels)
}
fn gamma_with(option: &Options, k: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
let underlying_price: Decimal = option.underlying_price.into();
let implied_volatility: Positive = option.implied_volatility;
let numerator = d_mul(
k.exp_minus_qt()?,
k.n_d1()?,
"greeks::gamma::discounted_pdf",
)?;
let denominator = d_mul(
underlying_price,
implied_volatility.to_dec(),
"greeks::gamma::price_vol",
)?;
let denominator = d_mul(
denominator,
k.sqrt_t().to_dec(),
"greeks::gamma::price_vol_time",
)?;
let gamma: Decimal = d_div(numerator, denominator, "greeks::gamma")?;
Ok(d_mul(
gamma,
signed_quantity(option),
"greeks::gamma::position_weighted",
)?)
}
pub fn theta(option: &Options) -> Result<Decimal, GreeksError> {
let t = option.expiration_date.get_years()?;
if t == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let kernels = BlackScholesKernels::new(option, t)?;
theta_with(option, &kernels)
}
fn theta_with(option: &Options, kernels: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
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 exp_minus_rt = kernels.exp_minus_rt()?;
let exp_minus_qt = kernels.exp_minus_qt()?;
let decay = d_mul(exp_minus_qt, s, "greeks::theta::decay_spot")?;
let decay = d_mul(decay, kernels.n_d1()?, "greeks::theta::decay_pdf")?;
let decay = d_mul(decay, sigma, "greeks::theta::decay_vol")?;
let decay_denominator = d_mul(
Decimal::TWO,
kernels.sqrt_t().to_dec(),
"greeks::theta::decay_time",
)?;
let common_term = -d_div(decay, decay_denominator, "greeks::theta::decay")?;
let rate_term = d_mul(r, k, "greeks::theta::rate_strike")?;
let rate_term = d_mul(rate_term, exp_minus_rt, "greeks::theta::rate_discounted")?;
let carry_term = d_mul(q, s, "greeks::theta::carry_spot")?;
let carry_term = d_mul(carry_term, exp_minus_qt, "greeks::theta::carry_discounted")?;
let theta = match option.option_style {
OptionStyle::Call => {
let rate = d_mul(rate_term, kernels.big_n_d2()?, "greeks::theta::call_rate")?;
let carry = d_mul(carry_term, kernels.big_n_d1()?, "greeks::theta::call_carry")?;
let theta = d_sub(common_term, rate, "greeks::theta::call_decay_rate")?;
d_add(theta, carry, "greeks::theta::call")?
}
OptionStyle::Put => {
let rate = d_mul(
rate_term,
kernels.big_n_neg_d2()?,
"greeks::theta::put_rate",
)?;
let carry = d_mul(
carry_term,
kernels.big_n_neg_d1()?,
"greeks::theta::put_carry",
)?;
let theta = d_add(common_term, rate, "greeks::theta::put_decay_rate")?;
d_sub(theta, carry, "greeks::theta::put")?
}
};
let weighted = d_mul(
theta,
signed_quantity(option),
"greeks::theta::position_weighted",
)?;
Ok(d_div(
weighted,
Decimal::from(365),
"greeks::theta::per_day",
)?)
}
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 kernels = BlackScholesKernels::new(option, expiration_date)?;
vega_with(option, &kernels)
}
fn vega_with(option: &Options, k: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
let underlying_price: Decimal = option.underlying_price.to_dec();
let vega = d_mul(
underlying_price,
k.exp_minus_qt()?,
"greeks::vega::discounted_spot",
)?;
let vega = d_mul(vega, k.n_d1()?, "greeks::vega::pdf")?;
let vega = d_mul(vega, k.sqrt_t().to_dec(), "greeks::vega::sqrt_time")?;
let vega: Decimal = d_div(vega, Decimal::ONE_HUNDRED, "greeks::vega::per_percent")?;
Ok(d_mul(
vega,
signed_quantity(option),
"greeks::vega::position_weighted",
)?)
}
pub fn rho(option: &Options) -> Result<Decimal, GreeksError> {
let t = option.expiration_date.get_years()?;
if t == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let kernels = BlackScholesKernels::new(option, t)?;
rho_with(option, &kernels)
}
fn rho_with(option: &Options, kernels: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
let t = kernels.t();
let k = option.strike_price.to_dec();
let base_rho = d_mul(k, t.to_dec(), "greeks::rho::strike_time")?;
let base_rho = d_mul(base_rho, kernels.exp_minus_rt()?, "greeks::rho::discounted")?;
let rho = match option.option_style {
OptionStyle::Call => d_mul(base_rho, kernels.big_n_d2()?, "greeks::rho::call")?,
OptionStyle::Put => d_mul(-base_rho, kernels.big_n_neg_d2()?, "greeks::rho::put")?,
};
let weighted = d_mul(
rho,
signed_quantity(option),
"greeks::rho::position_weighted",
)?;
Ok(d_div(
weighted,
Decimal::from(100),
"greeks::rho::per_basis_point",
)?)
}
pub fn rho_d(option: &Options) -> Result<Decimal, GreeksError> {
let expiration_date: Positive = option.expiration_date.get_years()?;
if expiration_date == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let kernels = BlackScholesKernels::new(option, expiration_date)?;
rho_d_with(option, &kernels)
}
fn rho_d_with(option: &Options, k: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
let expiration_date = k.t();
let underlying_price: Decimal = option.underlying_price.to_dec();
let base = d_mul(
expiration_date.to_dec(),
underlying_price,
"greeks::rho_d::time_spot",
)?;
let base = d_mul(base, k.exp_minus_qt()?, "greeks::rho_d::discounted")?;
let rhod = match option.option_style {
OptionStyle::Call => d_mul(-base, k.big_n_d1()?, "greeks::rho_d::call")?,
OptionStyle::Put => d_mul(base, k.big_n_neg_d1()?, "greeks::rho_d::put")?,
};
let weighted = d_mul(
rhod,
signed_quantity(option),
"greeks::rho_d::position_weighted",
)?;
Ok(d_div(
weighted,
Decimal::from(100),
"greeks::rho_d::per_basis_point",
)?)
}
pub fn alpha(option: &Options) -> Result<Decimal, GreeksError> {
let gamma = gamma(option)?;
let theta = theta(option)?;
alpha_from(gamma, theta)
}
fn alpha_from(gamma: Decimal, theta: Decimal) -> Result<Decimal, GreeksError> {
match (gamma, theta) {
(val, _) if val == Decimal::ZERO => Ok(Decimal::ZERO),
(_, val) if val == Decimal::ZERO => Ok(Decimal::MAX),
_ => Ok(d_div(gamma, theta, "greeks::alpha")?),
}
}
#[derive(Clone, Debug, Default)]
struct AlphaSum {
total: Decimal,
contributed: bool,
sentinel: Option<SentinelLeg>,
}
#[derive(Clone, Debug)]
struct SentinelLeg {
index: usize,
description: String,
}
impl AlphaSum {
fn push(
mut self,
value: Decimal,
option: &Options,
index: usize,
op: &'static str,
) -> Result<Self, GreeksError> {
if value == Decimal::MAX {
if self.contributed || self.sentinel.is_some() {
return Err(alpha_sentinel_error(index, &describe_leg(option)));
}
self.sentinel = Some(SentinelLeg {
index,
description: describe_leg(option),
});
self.total = Decimal::MAX;
return Ok(self);
}
if value.is_zero() {
return Ok(self);
}
if let Some(sentinel) = &self.sentinel {
return Err(alpha_sentinel_error(sentinel.index, &sentinel.description));
}
self.contributed = true;
self.total = d_add(self.total, value, op)?;
Ok(self)
}
}
fn describe_leg(option: &Options) -> String {
format!(
"{symbol} {strike} {style:?} {side:?}",
symbol = option.underlying_symbol,
strike = option.strike_price,
style = option.option_style,
side = option.side,
)
}
#[cold]
#[inline(never)]
fn alpha_sentinel_error(index: usize, description: &str) -> GreeksError {
GreeksError::CalculationError(CalculationErrorKind::ThetaError {
reason: format!(
"leg {index} ({description}) has a vanished theta, so its alpha is the \
Decimal::MAX sentinel and cannot be summed into an aggregate; read the \
leg's own alpha instead"
),
})
}
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()?;
if expiration_date == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let kernels = BlackScholesKernels::new(option, expiration_date)?;
vanna_with(option, &kernels)
}
fn vanna_with(option: &Options, k: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
let implied_volatility: Positive = option.implied_volatility;
let standardised_d2 = d_div(
k.d2()?,
implied_volatility.to_dec(),
"greeks::vanna::d2_over_sigma",
)?;
let vanna = d_mul(
k.exp_minus_qt()?,
k.n_d1()?,
"greeks::vanna::discounted_pdf",
)?;
let vanna: Decimal = -d_mul(vanna, standardised_d2, "greeks::vanna")?;
Ok(d_mul(
vanna,
signed_quantity(option),
"greeks::vanna::position_weighted",
)?)
}
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 kernels = BlackScholesKernels::new(option, expiration_date)?;
vomma_with(option, &kernels)
}
fn vomma_with(option: &Options, k: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
let vega = vega_with(option, k)?;
let implied_volatility: Positive = option.implied_volatility;
let d1_d2 = d_mul(k.d1(), k.d2()?, "greeks::vomma::d1_d2")?;
let scaled = d_div(
d1_d2,
implied_volatility.to_dec(),
"greeks::vomma::d1_d2_over_sigma",
)?;
Ok(d_mul(vega, scaled, "greeks::vomma")?)
}
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 kernels = BlackScholesKernels::new(option, expiration_date)?;
veta_with(option, &kernels)
}
fn veta_with(option: &Options, k: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
let expiration_date = k.t();
let vega = vega_with(option, k)?;
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 carry = d_sub(risk_free_rate, dividend_yield, "greeks::veta::carry")?;
let add1_numerator = d_mul(carry, k.d1(), "greeks::veta::carry_d1")?;
let add1_denominator = d_mul(
implied_volatility.to_dec(),
k.sqrt_t().to_dec(),
"greeks::veta::vol_time",
)?;
let add1 = d_div(add1_numerator, add1_denominator, "greeks::veta::carry_term")?;
let d1_d2 = d_mul(k.d1(), k.d2()?, "greeks::veta::d1_d2")?;
let add2_numerator = d_add(Decimal::ONE, d1_d2, "greeks::veta::one_plus_d1_d2")?;
let add2_denominator = d_mul(
Decimal::TWO,
expiration_date.to_dec(),
"greeks::veta::two_tau",
)?;
let add2 = d_div(add2_numerator, add2_denominator, "greeks::veta::time_term")?;
let bracket = d_add(dividend_yield, add1, "greeks::veta::bracket_carry")?;
let bracket = d_sub(bracket, add2, "greeks::veta::bracket")?;
let veta: Decimal = d_mul(-vega, bracket, "greeks::veta")?;
let scale = d_mul(
TRADING_DAYS.to_dec(),
Decimal::ONE_HUNDRED,
"greeks::veta::scale",
)?;
Ok(d_div(veta, scale, "greeks::veta::per_day_percent")?)
}
pub fn charm(option: &Options) -> Result<Decimal, GreeksError> {
let tau = option.expiration_date.get_years()?;
if tau == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let kernels = BlackScholesKernels::new(option, tau)?;
charm_with(option, &kernels)
}
fn charm_with(option: &Options, k: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
let tau = k.t();
let r = option.risk_free_rate;
let q = option.dividend_yield.to_dec();
let sigma = option.implied_volatility;
let exp_minus_qt = k.exp_minus_qt()?;
let carry = d_sub(r, q, "greeks::charm::carry")?;
let carry_term = d_mul(Decimal::TWO, carry, "greeks::charm::two_carry")?;
let carry_term = d_mul(carry_term, tau.to_dec(), "greeks::charm::carry_tau")?;
let vol_time = d_mul(
sigma.to_dec(),
k.sqrt_t().to_dec(),
"greeks::charm::vol_time",
)?;
let d2_term = d_mul(k.d2()?, vol_time, "greeks::charm::d2_vol_time")?;
let numerator = d_sub(carry_term, d2_term, "greeks::charm::numerator")?;
let denominator = d_mul(Decimal::TWO, tau.to_dec(), "greeks::charm::two_tau")?;
let denominator = d_mul(denominator, vol_time, "greeks::charm::denominator")?;
let common_term = d_div(numerator, denominator, "greeks::charm::common_term")?;
let pdf_term = d_mul(exp_minus_qt, k.n_d1()?, "greeks::charm::discounted_pdf")?;
let pdf_term = d_mul(pdf_term, common_term, "greeks::charm::pdf_common")?;
let charm = match option.option_style {
OptionStyle::Call => {
let carry_leg = d_mul(q, exp_minus_qt, "greeks::charm::call_carry")?;
let carry_leg = d_mul(carry_leg, k.big_n_d1()?, "greeks::charm::call_carry_cdf")?;
d_sub(carry_leg, pdf_term, "greeks::charm::call")?
}
OptionStyle::Put => {
let carry_leg = d_mul(-q, exp_minus_qt, "greeks::charm::put_carry")?;
let carry_leg = d_mul(carry_leg, k.big_n_neg_d1()?, "greeks::charm::put_carry_cdf")?;
d_sub(carry_leg, pdf_term, "greeks::charm::put")?
}
};
let weighted = d_mul(
charm,
signed_quantity(option),
"greeks::charm::position_weighted",
)?;
Ok(d_div(
weighted,
Decimal::from(365),
"greeks::charm::per_day",
)?)
}
pub fn color(option: &Options) -> Result<Decimal, GreeksError> {
let tau = option.expiration_date.get_years()?;
if tau == Decimal::ZERO {
return Ok(Decimal::ZERO);
}
let kernels = BlackScholesKernels::new(option, tau)?;
color_with(option, &kernels)
}
fn color_with(option: &Options, k: &BlackScholesKernels) -> Result<Decimal, GreeksError> {
let tau = k.t();
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 = k.exp_minus_qt()?;
let scale = d_mul(Decimal::TWO, s.to_dec(), "greeks::color::two_spot")?;
let scale = d_mul(scale, tau.to_dec(), "greeks::color::two_spot_tau")?;
let vol_time = d_mul(
sigma.to_dec(),
k.sqrt_t().to_dec(),
"greeks::color::vol_time",
)?;
let scale = d_mul(scale, vol_time, "greeks::color::factor1_denominator")?;
let factor1 = d_div(k.n_d1()?, scale, "greeks::color::factor1")?;
let carry = d_sub(r, q, "greeks::color::carry")?;
let carry_term = d_mul(Decimal::TWO, carry, "greeks::color::two_carry")?;
let carry_term = d_mul(carry_term, tau.to_dec(), "greeks::color::carry_tau")?;
let d2_term = d_mul(k.d2()?, vol_time, "greeks::color::d2_vol_time")?;
let numerator = d_sub(carry_term, d2_term, "greeks::color::factor2_numerator")?;
let ratio = d_div(numerator, vol_time, "greeks::color::factor2_ratio")?;
let ratio = d_mul(ratio, k.d1(), "greeks::color::factor2_ratio_d1")?;
let dividend_term = d_mul(Decimal::TWO, q, "greeks::color::two_q")?;
let dividend_term = d_mul(dividend_term, tau.to_dec(), "greeks::color::two_q_tau")?;
let factor2 = d_add(dividend_term, Decimal::ONE, "greeks::color::factor2_base")?;
let factor2 = d_add(factor2, ratio, "greeks::color::factor2")?;
let numerator = d_mul(
-exp_minus_qt,
factor1,
"greeks::color::numerator_exp_factor1",
)?;
let numerator = d_mul(numerator, factor2, "greeks::color::numerator_factor2")?;
let numerator = d_mul(
numerator,
signed_quantity(option),
"greeks::color::numerator_quantity",
)?;
let color = d_div(numerator, Decimal::from(365), "greeks::color::per_day")?;
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.4653476616529870686572684641),
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.518125955681732, 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.3231079315892283130741442305),
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.2298186207440564194124373536),
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.000016992916331106763, 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.06926321174822156, 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.05754408301594555, 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.03569396592472471, 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.002147363766511278, 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.05569703183000544;
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.05620624081929407;
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.24314466256999295;
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.013947672323606776;
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.05569703183000544;
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.05620624081929407;
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.24314466256999295;
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.013947672323606776;
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_at(
side: Side,
style: OptionStyle,
quantity: Positive,
expiration_date: ExpirationDate,
) -> Options {
Options::new(
OptionType::European,
side,
"TEST".to_string(),
Positive::HUNDRED,
expiration_date,
pos_or_panic!(0.2),
quantity,
Positive::HUNDRED,
dec!(0.05),
style,
pos_or_panic!(0.01),
None,
)
}
#[test]
fn test_rho_d_and_vanna_are_zero_at_expiry() {
let option = create_test_option_at(
Side::Long,
OptionStyle::Call,
Positive::ONE,
ExpirationDate::Days(Positive::ZERO),
);
match rho_d(&option) {
Ok(value) => assert_eq!(value, Decimal::ZERO),
Err(e) => panic!("rho_d should be zero at expiry, got {e}"),
}
match vanna(&option) {
Ok(value) => assert_eq!(value, Decimal::ZERO),
Err(e) => panic!("vanna should be zero at expiry, got {e}"),
}
}
#[test]
fn test_greeks_succeeds_at_expiry() {
for style in [OptionStyle::Call, OptionStyle::Put] {
let option = create_test_option_at(
Side::Long,
style,
Positive::ONE,
ExpirationDate::Days(Positive::ZERO),
);
let greek = match option.greeks() {
Ok(greek) => greek,
Err(e) => panic!("greeks() should succeed at expiry for {style:?}: {e}"),
};
assert_eq!(greek.gamma, Decimal::ZERO);
assert_eq!(greek.theta, Decimal::ZERO);
assert_eq!(greek.vega, Decimal::ZERO);
assert_eq!(greek.rho_d, Decimal::ZERO);
assert_eq!(greek.vanna, Decimal::ZERO);
}
}
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.5338307582207135564475476937),
dec!(0.000001)
);
assert_decimal_eq!(
greeks.gamma,
dec!(0.0692632117482215620683508231),
dec!(0.000001)
);
assert_decimal_eq!(
greeks.theta,
dec!(-0.0434671314177636287945041349),
dec!(0.000001)
);
assert_decimal_eq!(
greeks.vega,
dec!(0.1138573343806381728362131205),
dec!(0.000001)
);
assert_decimal_eq!(
greeks.rho,
dec!(0.041863419880440417503050762),
dec!(0.000001)
);
assert_decimal_eq!(
greeks.rho_d,
dec!(-0.0438765006756750824436552223),
dec!(0.000001)
);
assert_decimal_eq!(
greeks.vanna,
dec!(-0.0569286671903190864181065602),
dec!(0.000001)
);
assert_decimal_eq!(
greeks.vomma,
dec!(0.0014037205608571828124031742),
dec!(0.000001)
);
assert_decimal_eq!(
greeks.veta,
dec!(0.000027236903336955576237203),
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();
for (name, value) in [
("gamma", greeks.gamma),
("vega", greeks.vega),
("vanna", greeks.vanna),
("vomma", greeks.vomma),
("veta", greeks.veta),
("color", greeks.color),
] {
assert_eq!(
value,
Decimal::ZERO,
"{name} has no style branch, so a synthetic forward cancels it"
);
}
for (name, value) in [
("delta", greeks.delta),
("theta", greeks.theta),
("rho", greeks.rho),
("rho_d", greeks.rho_d),
("charm", greeks.charm),
] {
assert!(
value.abs() > Decimal::ZERO,
"{name} branches on style, so it must survive the synthetic forward"
);
}
}
#[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.9439386484253192473553911946),
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_dividend_high_q_carry_regression() {
let option = Options::new(
OptionType::European,
Side::Long,
"HIQ".to_string(),
pos_or_panic!(100.0),
ExpirationDate::Days(pos_or_panic!(365.0)),
pos_or_panic!(0.3),
Positive::ONE,
pos_or_panic!(100.0),
dec!(0.05),
OptionStyle::Call,
pos_or_panic!(0.08),
None,
);
let g = option.greeks().unwrap();
assert_decimal_eq!(g.delta, dec!(0.4799640107901480920925461492), dec!(1e-12));
assert_decimal_eq!(g.gamma, dec!(0.0122603363406382730603468554), dec!(1e-12));
assert_decimal_eq!(g.theta, dec!(-0.0098247973187618777368017226), dec!(1e-12));
assert_decimal_eq!(g.vega, dec!(0.3678100902191481918104056619), dec!(1e-12));
assert_decimal_eq!(g.rho, dec!(0.381722350876409446703382601), dec!(1e-12));
assert_decimal_eq!(g.rho_d, dec!(-0.4799640107901480920925461492), dec!(1e-12));
assert_decimal_eq!(g.vanna, dec!(0.3065084085159568265086713849), dec!(1e-12));
assert_decimal_eq!(g.vomma, dec!(-0.0153254204257978413254335693), dec!(1e-12));
assert_decimal_eq!(g.veta, dec!(0.0000061119236221931867190717), dec!(1e-12));
assert_decimal_eq!(g.charm, dec!(0.0000800051194732414864990234), dec!(1e-12));
assert_decimal_eq!(g.color, dec!(-0.000019524165747934236209114), dec!(1e-12));
}
#[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_eq!(greeks.delta, Decimal::ZERO);
assert_eq!(greeks.gamma, Decimal::ZERO);
assert_eq!(greeks.theta, Decimal::ZERO);
assert_eq!(greeks.vega, Decimal::ZERO);
assert_eq!(greeks.rho, Decimal::ZERO);
assert_eq!(greeks.rho_d, Decimal::ZERO);
assert_eq!(greeks.vanna, Decimal::ZERO);
assert_eq!(greeks.vomma, Decimal::ZERO);
assert_eq!(greeks.veta, Decimal::ZERO);
assert_eq!(greeks.charm, Decimal::ZERO);
assert_eq!(greeks.color, Decimal::ZERO);
}
#[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_eq!(gamma, Decimal::ZERO, "gamma should cancel");
assert_eq!(vega, Decimal::ZERO, "vega should cancel");
assert_eq!(vanna, Decimal::ZERO, "vanna should cancel");
assert_eq!(vomma, Decimal::ZERO, "vomma should cancel");
assert_eq!(veta, Decimal::ZERO, "veta should cancel");
assert!(delta.abs() > Decimal::ZERO, "Delta calculation failed");
assert!(theta.abs() > Decimal::ZERO, "Theta calculation failed");
assert!(rho.abs() > Decimal::ZERO, "Rho calculation failed");
assert!(rho_d.abs() > Decimal::ZERO, "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));
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.2814302601877034;
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.7399563431070563;
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.015658567140361693;
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.0;
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.0;
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.00274096463168,
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.0039614286773,
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.000523300995754,
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.000550675746984,
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.00405183908388,
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.00282022266253,
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.000400671355466,
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.000817099264221,
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.378230424889,
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.4548921660125,
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.000452958052918,
epsilon = 1e-8
);
}
}
#[cfg(test)]
mod tests_shared_kernel_equivalence {
use super::*;
use crate::greeks::utils::d2 as fresh_d2_fn;
use crate::model::types::OptionType;
use crate::{ExpirationDate, Options};
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
fn branches() -> Vec<(&'static str, OptionType, Positive, Positive)> {
vec![
(
"live european",
OptionType::European,
pos_or_panic!(30.0),
pos_or_panic!(0.2),
),
(
"at expiry",
OptionType::European,
Positive::ZERO,
pos_or_panic!(0.2),
),
(
"zero volatility",
OptionType::European,
pos_or_panic!(30.0),
Positive::ZERO,
),
(
"non european",
OptionType::American,
pos_or_panic!(30.0),
pos_or_panic!(0.2),
),
]
}
fn option_for(
option_type: OptionType,
days: Positive,
implied_volatility: Positive,
style: OptionStyle,
side: Side,
quantity: Positive,
) -> Options {
Options::new(
option_type,
side,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(days),
implied_volatility,
quantity,
pos_or_panic!(105.0),
dec!(0.05),
style,
pos_or_panic!(0.02),
None,
)
}
#[test]
fn test_greeks_matches_the_individual_functions_exactly() {
for (name, option_type, days, iv) in branches() {
for style in [OptionStyle::Call, OptionStyle::Put] {
for side in [Side::Long, Side::Short] {
let option = option_for(
option_type.clone(),
days,
iv,
style,
side,
pos_or_panic!(3.0),
);
let label = format!("{name} {style:?} {side:?}");
let individual = [
("delta", delta(&option)),
("gamma", gamma(&option)),
("theta", theta(&option)),
("vega", vega(&option)),
("rho", rho(&option)),
("rho_d", rho_d(&option)),
("alpha", alpha(&option)),
("vanna", vanna(&option)),
("vomma", vomma(&option)),
("veta", veta(&option)),
("charm", charm(&option)),
("color", color(&option)),
];
let all_ok = individual.iter().all(|(_, r)| r.is_ok());
match (option.greeks(), all_ok) {
(Ok(aggregate), true) => {
let values = [
aggregate.delta,
aggregate.gamma,
aggregate.theta,
aggregate.vega,
aggregate.rho,
aggregate.rho_d,
aggregate.alpha,
aggregate.vanna,
aggregate.vomma,
aggregate.veta,
aggregate.charm,
aggregate.color,
];
for ((greek, single), aggregated) in
individual.iter().zip(values.iter())
{
assert_eq!(
aggregated,
&expect(single, &label),
"{greek} disagrees for {label}"
);
}
}
(Err(_), false) => {
}
(Ok(_), false) => {
panic!("greeks() succeeded for {label} where an individual greek fails")
}
(Err(e), true) => panic!(
"greeks() failed for {label} where every individual greek succeeds: {e}"
),
}
}
}
}
}
fn expect(result: &Result<Decimal, GreeksError>, label: &str) -> Decimal {
match result {
Ok(value) => *value,
Err(e) => panic!("individual greek failed for {label}: {e}"),
}
}
#[test]
fn test_greeks_aggregation_is_order_independent() {
struct Legs(Vec<Options>);
impl Greeks for Legs {
fn get_options(&self) -> Result<Vec<&Options>, GreeksError> {
Ok(self.0.iter().collect())
}
}
let legs = Legs(vec![
option_for(
OptionType::European,
pos_or_panic!(30.0),
pos_or_panic!(0.2),
OptionStyle::Call,
Side::Long,
pos_or_panic!(2.0),
),
option_for(
OptionType::European,
pos_or_panic!(45.0),
pos_or_panic!(0.35),
OptionStyle::Put,
Side::Short,
pos_or_panic!(3.0),
),
option_for(
OptionType::European,
Positive::ZERO,
pos_or_panic!(0.2),
OptionStyle::Call,
Side::Short,
Positive::ONE,
),
]);
let Ok(total) = legs.greeks() else {
panic!("aggregate greeks should succeed");
};
let mut delta_sum = Decimal::ZERO;
let mut charm_sum = Decimal::ZERO;
let mut alpha_sum = Decimal::ZERO;
for option in &legs.0 {
delta_sum += expect(&delta(option), "leg");
charm_sum += expect(&charm(option), "leg");
alpha_sum += expect(&alpha(option), "leg");
}
assert_eq!(total.delta, delta_sum);
assert_eq!(total.charm, charm_sum);
assert_eq!(total.alpha, alpha_sum);
}
#[test]
fn test_cached_kernels_match_freshly_derived_values() {
let option = option_for(
OptionType::European,
pos_or_panic!(30.0),
pos_or_panic!(0.2),
OptionStyle::Call,
Side::Long,
Positive::ONE,
);
let Ok(t) = option.expiration_date.get_years() else {
panic!("expiration should resolve");
};
let Ok(kernels) = BlackScholesKernels::new(&option, t) else {
panic!("kernels should build for a live european option");
};
let carry = option.risk_free_rate - option.dividend_yield.to_dec();
let Ok(fresh_d1) = d1(
option.underlying_price,
option.strike_price,
carry,
t,
option.implied_volatility,
) else {
panic!("d1 should compute");
};
let Ok(fresh_d2) = fresh_d2_fn(
option.underlying_price,
option.strike_price,
carry,
t,
option.implied_volatility,
) else {
panic!("d2 should compute");
};
assert_eq!(kernels.d1(), fresh_d1, "d1");
assert_eq!(kernels.d2().ok(), Some(fresh_d2), "d2 derived from d1");
assert_eq!(kernels.sqrt_t(), t.sqrt(), "sqrt(T)");
assert_eq!(kernels.n_d1().ok(), n(fresh_d1).ok(), "n(d1)");
assert_eq!(kernels.big_n_d1().ok(), big_n(fresh_d1).ok(), "big_n(d1)");
assert_eq!(
kernels.big_n_neg_d1().ok(),
big_n(-fresh_d1).ok(),
"big_n(-d1)"
);
assert_eq!(kernels.big_n_d2().ok(), big_n(fresh_d2).ok(), "big_n(d2)");
assert_eq!(
kernels.big_n_neg_d2().ok(),
big_n(-fresh_d2).ok(),
"big_n(-d2)"
);
assert_eq!(
kernels.exp_minus_qt().ok(),
d_exp(-t.to_dec() * option.dividend_yield, "test::exp_minus_qt").ok(),
"exp(-qT)"
);
assert_eq!(
kernels.exp_minus_rt().ok(),
d_exp(-option.risk_free_rate * t, "test::exp_minus_rt").ok(),
"exp(-rT)"
);
}
}
#[cfg(test)]
mod tests_side_sign_convention {
use super::*;
use crate::model::types::OptionType;
use crate::{ExpirationDate, Options};
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
fn option(style: OptionStyle, side: Side, 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,
pos_or_panic!(105.0),
dec!(0.05),
style,
pos_or_panic!(0.02),
None,
)
}
fn ok(result: Result<Decimal, GreeksError>, what: &str) -> Decimal {
match result {
Ok(value) => value,
Err(e) => panic!("{what} should compute: {e}"),
}
}
fn binary_option(side: Side, quantity: Positive) -> Options {
Options::new(
OptionType::Binary {
binary_type: crate::model::types::BinaryType::CashOrNothing,
},
side,
"TEST".to_string(),
Positive::HUNDRED,
ExpirationDate::Days(pos_or_panic!(30.0)),
pos_or_panic!(0.2),
quantity,
pos_or_panic!(105.0),
dec!(0.05),
OptionStyle::Call,
pos_or_panic!(0.02),
None,
)
}
#[test]
fn test_non_european_fallback_is_signed_and_scaled() {
let one_long = binary_option(Side::Long, Positive::ONE);
let Ok(delta_one) = delta(&one_long) else {
panic!("a binary option should price through the numerical fallback");
};
let Ok(gamma_one) = gamma(&one_long) else {
panic!("a binary option should price through the numerical fallback");
};
assert!(
delta_one != Decimal::ZERO,
"the fixture must produce a non-zero delta for this test to mean anything"
);
let three_long = binary_option(Side::Long, pos_or_panic!(3.0));
assert_eq!(ok(delta(&three_long), "delta"), delta_one * dec!(3));
assert_eq!(ok(gamma(&three_long), "gamma"), gamma_one * dec!(3));
let three_short = binary_option(Side::Short, pos_or_panic!(3.0));
assert_eq!(ok(delta(&three_short), "delta"), -delta_one * dec!(3));
assert_eq!(ok(gamma(&three_short), "gamma"), -gamma_one * dec!(3));
}
#[test]
fn test_signed_quantity_carries_the_side() {
for quantity in [Positive::ONE, Positive::TWO, pos_or_panic!(7.5)] {
let long = option(OptionStyle::Call, Side::Long, quantity);
let short = option(OptionStyle::Call, Side::Short, quantity);
assert_eq!(signed_quantity(&long), quantity.to_dec());
assert_eq!(signed_quantity(&short), -quantity.to_dec());
}
}
#[test]
fn test_flipping_the_side_negates_every_greek_except_alpha() {
for style in [OptionStyle::Call, OptionStyle::Put] {
let long = option(style, Side::Long, pos_or_panic!(3.0));
let short = option(style, Side::Short, pos_or_panic!(3.0));
for (name, l, s) in [
("delta", delta(&long), delta(&short)),
("gamma", gamma(&long), gamma(&short)),
("theta", theta(&long), theta(&short)),
("vega", vega(&long), vega(&short)),
("rho", rho(&long), rho(&short)),
("rho_d", rho_d(&long), rho_d(&short)),
("vanna", vanna(&long), vanna(&short)),
("vomma", vomma(&long), vomma(&short)),
("veta", veta(&long), veta(&short)),
("charm", charm(&long), charm(&short)),
("color", color(&long), color(&short)),
] {
let long_value = ok(l, name);
assert_eq!(
ok(s, name),
-long_value,
"{name} should negate when the side flips, for {style:?}"
);
}
assert_eq!(
ok(alpha(&short), "alpha"),
ok(alpha(&long), "alpha"),
"alpha is a ratio and must not change with the side"
);
}
}
#[test]
fn test_offsetting_legs_net_to_zero() {
struct Legs(Vec<Options>);
impl Greeks for Legs {
fn get_options(&self) -> Result<Vec<&Options>, GreeksError> {
Ok(self.0.iter().collect())
}
}
let legs = Legs(vec![
option(OptionStyle::Call, Side::Long, pos_or_panic!(4.0)),
option(OptionStyle::Call, Side::Short, pos_or_panic!(4.0)),
]);
let Ok(g) = legs.greeks() else {
panic!("aggregate should compute");
};
for (name, value) in [
("delta", g.delta),
("gamma", g.gamma),
("theta", g.theta),
("vega", g.vega),
("rho", g.rho),
("rho_d", g.rho_d),
("vanna", g.vanna),
("vomma", g.vomma),
("veta", g.veta),
("charm", g.charm),
("color", g.color),
] {
assert_eq!(value, Decimal::ZERO, "{name} should net to zero");
}
}
#[test]
fn test_short_premium_theta_is_positive() {
let short_call = option(OptionStyle::Call, Side::Short, Positive::ONE);
assert!(
ok(theta(&short_call), "theta").is_sign_positive(),
"a short call collects decay, so theta must be positive"
);
let long_call = option(OptionStyle::Call, Side::Long, Positive::ONE);
assert!(
ok(theta(&long_call), "theta").is_sign_negative(),
"a long call pays decay, so theta must be negative"
);
}
}
#[cfg(test)]
mod tests_checked_aggregation {
use super::*;
use crate::model::types::OptionType;
use crate::{ExpirationDate, Options};
use positive::{Positive, pos_or_panic};
use rust_decimal_macros::dec;
#[test]
fn test_alpha_sum_refuses_the_sentinel_whatever_the_leg_order() {
let option = leg(
OptionType::European,
Positive::HUNDRED,
pos_or_panic!(30.0),
pos_or_panic!(0.2),
OptionStyle::Call,
Side::Long,
Positive::ONE,
);
let fold = |values: &[Decimal]| -> Result<Decimal, GreeksError> {
let mut sum = AlphaSum::default();
for (index, value) in values.iter().enumerate() {
sum = sum.push(*value, &option, index, "test::alpha_sum")?;
}
Ok(sum.total)
};
assert!(fold(&[dec!(5), dec!(-5), Decimal::MAX]).is_err());
assert!(fold(&[Decimal::MAX, dec!(5), dec!(-5)]).is_err());
assert_eq!(fold(&[Decimal::MAX]).unwrap(), Decimal::MAX);
assert_eq!(
fold(&[Decimal::ZERO, Decimal::MAX, Decimal::ZERO]).unwrap(),
Decimal::MAX
);
assert_eq!(fold(&[dec!(5), dec!(-5)]).unwrap(), Decimal::ZERO);
}
const SENTINEL_QUANTITY: Decimal = Decimal::from_parts(1, 0, 0, false, 27);
struct Legs(Vec<Options>);
impl Greeks for Legs {
fn get_options(&self) -> Result<Vec<&Options>, GreeksError> {
Ok(self.0.iter().collect())
}
}
fn positive(value: Decimal) -> Positive {
match Positive::new_decimal(value) {
Ok(v) => v,
Err(e) => panic!("fixture quantity should be positive: {e}"),
}
}
fn leg(
option_type: OptionType,
strike: Positive,
days: Positive,
implied_volatility: Positive,
style: OptionStyle,
side: Side,
quantity: Positive,
) -> Options {
Options::new(
option_type,
side,
"TEST".to_string(),
strike,
ExpirationDate::Days(days),
implied_volatility,
quantity,
Positive::HUNDRED,
dec!(0.05),
style,
Positive::ZERO,
None,
)
}
fn sentinel_leg() -> Options {
leg(
OptionType::European,
Positive::HUNDRED,
pos_or_panic!(30.0),
pos_or_panic!(0.2),
OptionStyle::Call,
Side::Long,
positive(SENTINEL_QUANTITY),
)
}
#[test]
fn test_alpha_sentinel_refusal_names_the_sentinel_leg_in_either_order() {
let sentinel = sentinel_leg();
let ordinary = leg(
OptionType::European,
Positive::HUNDRED,
pos_or_panic!(30.0),
pos_or_panic!(0.8),
OptionStyle::Put,
Side::Long,
Positive::ONE,
);
assert!(
ok(alpha(&ordinary), "alpha") != Decimal::ZERO,
"the ordinary leg must contribute, or nothing is being refused"
);
for (legs, sentinel_index, what) in [
(
vec![sentinel.clone(), ordinary.clone()],
"leg 0",
"sentinel first",
),
(
vec![ordinary.clone(), sentinel.clone()],
"leg 1",
"sentinel last",
),
] {
match Legs(legs).alpha() {
Err(GreeksError::CalculationError(CalculationErrorKind::ThetaError { reason })) => {
assert!(
reason.contains(sentinel_index) && reason.contains("Call"),
"{what}: the refusal must name the sentinel leg, got {reason}"
);
assert!(
!reason.contains("Put"),
"{what}: the refusal must not blame the ordinary leg, got {reason}"
);
}
other => panic!("{what}: expected the sentinel refusal, got {other:?}"),
}
}
}
fn ok(result: Result<Decimal, GreeksError>, what: &str) -> Decimal {
match result {
Ok(value) => value,
Err(e) => panic!("{what} should compute: {e}"),
}
}
fn expect_sentinel_refusal(result: Result<Decimal, GreeksError>, what: &str) {
match result {
Ok(value) => panic!("{what} should be refused, got {value}"),
Err(GreeksError::CalculationError(CalculationErrorKind::ThetaError { reason })) => {
assert!(
reason.contains("leg ") && reason.contains("sentinel"),
"the refusal should name the leg and the sentinel, got {reason}"
);
}
Err(e) => panic!("{what} should report the sentinel refusal, got {e}"),
}
}
#[test]
fn test_alpha_sub_contract_quantity_returns_the_sentinel() {
let option = sentinel_leg();
assert_eq!(
ok(theta(&option), "theta"),
Decimal::ZERO,
"the fixture's daily theta must round to zero"
);
assert_ne!(
ok(gamma(&option), "gamma"),
Decimal::ZERO,
"the fixture's gamma must stay non-zero, or alpha takes the zero branch"
);
assert_eq!(
ok(alpha(&option), "alpha"),
Decimal::MAX,
"a vanished theta against a live gamma is the sentinel"
);
}
#[test]
fn test_alpha_two_sentinel_legs_are_refused() {
let legs = Legs(vec![sentinel_leg(), sentinel_leg()]);
expect_sentinel_refusal(legs.alpha(), "the aggregate alpha of two sentinel legs");
}
#[test]
fn test_greeks_two_sentinel_legs_are_refused() {
let legs = Legs(vec![sentinel_leg(), sentinel_leg()]);
match legs.greeks() {
Ok(g) => panic!("greeks() should refuse two sentinel legs, got {g:?}"),
Err(GreeksError::CalculationError(CalculationErrorKind::ThetaError { reason })) => {
assert!(
reason.contains("sentinel"),
"the refusal should mention the sentinel, got {reason}"
);
}
Err(e) => panic!("greeks() should report the sentinel refusal, got {e}"),
}
}
#[test]
fn test_alpha_sentinel_beside_an_ordinary_leg_is_refused() {
let ordinary = leg(
OptionType::European,
Positive::HUNDRED,
pos_or_panic!(30.0),
pos_or_panic!(0.8),
OptionStyle::Call,
Side::Long,
Positive::ONE,
);
let ordinary_alpha = ok(alpha(&ordinary), "alpha");
assert!(
ordinary_alpha != Decimal::ZERO && ordinary_alpha.abs() < Decimal::ONE,
"the fixture must contribute a small non-zero alpha, got {ordinary_alpha}"
);
assert_eq!(
Decimal::MAX.checked_add(ordinary_alpha),
Some(Decimal::MAX),
"checked_add cannot detect this, so the guard must"
);
expect_sentinel_refusal(
Legs(vec![sentinel_leg(), ordinary.clone()]).alpha(),
"the aggregate alpha with the sentinel leg first",
);
expect_sentinel_refusal(
Legs(vec![ordinary, sentinel_leg()]).alpha(),
"the aggregate alpha with the sentinel leg last",
);
}
#[test]
fn test_alpha_one_sentinel_leg_still_returns_the_sentinel() {
let legs = Legs(vec![sentinel_leg()]);
assert_eq!(ok(legs.alpha(), "alpha"), Decimal::MAX);
let Ok(aggregate) = legs.greeks() else {
panic!("a lone sentinel leg should still aggregate");
};
assert_eq!(aggregate.alpha, Decimal::MAX);
}
#[test]
fn test_alpha_sentinel_beside_a_zero_alpha_leg_is_allowed() {
let expired = leg(
OptionType::European,
Positive::HUNDRED,
Positive::ZERO,
pos_or_panic!(0.2),
OptionStyle::Call,
Side::Long,
Positive::ONE,
);
assert_eq!(
ok(alpha(&expired), "alpha"),
Decimal::ZERO,
"the fixture must contribute exactly zero"
);
assert_eq!(
ok(Legs(vec![sentinel_leg(), expired.clone()]).alpha(), "alpha"),
Decimal::MAX
);
assert_eq!(
ok(Legs(vec![expired, sentinel_leg()]).alpha(), "alpha"),
Decimal::MAX
);
}
#[test]
fn test_checked_aggregation_matches_the_unchecked_sum() {
let mut options = Vec::new();
for style in [OptionStyle::Call, OptionStyle::Put] {
for side in [Side::Long, Side::Short] {
for strike in [pos_or_panic!(80.0), Positive::HUNDRED, pos_or_panic!(120.0)] {
options.push(leg(
OptionType::European,
strike,
pos_or_panic!(30.0),
pos_or_panic!(0.2),
style,
side,
pos_or_panic!(3.0),
));
options.push(leg(
OptionType::European,
strike,
pos_or_panic!(3650.0),
pos_or_panic!(0.8),
style,
side,
pos_or_panic!(0.25),
));
}
}
}
let legs = Legs(options);
let mut reference = Greek {
delta: Decimal::ZERO,
gamma: Decimal::ZERO,
theta: Decimal::ZERO,
vega: Decimal::ZERO,
rho: Decimal::ZERO,
rho_d: Decimal::ZERO,
alpha: Decimal::ZERO,
vanna: Decimal::ZERO,
vomma: Decimal::ZERO,
veta: Decimal::ZERO,
charm: Decimal::ZERO,
color: Decimal::ZERO,
};
for option in &legs.0 {
reference.delta += ok(delta(option), "delta");
reference.gamma += ok(gamma(option), "gamma");
reference.theta += ok(theta(option), "theta");
reference.vega += ok(vega(option), "vega");
reference.rho += ok(rho(option), "rho");
reference.rho_d += ok(rho_d(option), "rho_d");
reference.alpha += ok(alpha(option), "alpha");
reference.vanna += ok(vanna(option), "vanna");
reference.vomma += ok(vomma(option), "vomma");
reference.veta += ok(veta(option), "veta");
reference.charm += ok(charm(option), "charm");
reference.color += ok(color(option), "color");
}
let Ok(aggregate) = legs.greeks() else {
panic!("the aggregate should compute for ordinary legs");
};
assert_eq!(
aggregate, reference,
"greeks() must match the unchecked sum"
);
for (name, aggregated, expected) in [
("delta", legs.delta(), reference.delta),
("gamma", legs.gamma(), reference.gamma),
("theta", legs.theta(), reference.theta),
("vega", legs.vega(), reference.vega),
("rho", legs.rho(), reference.rho),
("rho_d", legs.rho_d(), reference.rho_d),
("alpha", legs.alpha(), reference.alpha),
("vanna", legs.vanna(), reference.vanna),
("vomma", legs.vomma(), reference.vomma),
("veta", legs.veta(), reference.veta),
("charm", legs.charm(), reference.charm),
("color", legs.color(), reference.color),
] {
assert_eq!(
ok(aggregated, name),
expected,
"{name} must match the unchecked sum"
);
}
}
#[test]
fn test_checked_aggregation_of_no_legs_is_zero() {
let legs = Legs(Vec::new());
assert_eq!(ok(legs.alpha(), "alpha"), Decimal::ZERO);
let Ok(aggregate) = legs.greeks() else {
panic!("an empty leg set should aggregate to zero");
};
assert_eq!(aggregate.delta, Decimal::ZERO);
assert_eq!(aggregate.alpha, Decimal::ZERO);
}
}