use crate::error::greeks::GreeksError;
use crate::pricing::{ClosedFormEngine, price_option_with};
use optionstratlib_core::model::Positive;
use optionstratlib_core::model::decimal::{d_add, d_div, d_mul, d_sub};
use optionstratlib_core::model::{ExpirationDate, Options};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
const H: Decimal = dec!(0.01);
const THETA_BUMP_DAYS: Decimal = dec!(1);
const THETA_SPAN_DAYS: Decimal = dec!(2);
const TWO_H: Decimal = dec!(0.020);
fn bump_up(x: Decimal, op: &'static str) -> Result<Decimal, GreeksError> {
Ok(d_add(x, H, op)?)
}
fn bump_down(x: Decimal, op: &'static str) -> Result<Decimal, GreeksError> {
Ok(d_sub(x, H, op)?)
}
pub fn numerical_delta(option: &Options) -> Result<Decimal, GreeksError> {
let mut opt_plus = option.clone();
opt_plus.underlying_price = Positive::new_decimal(
bump_up(
option.underlying_price.to_dec(),
"greeks::numerical::spot_up",
)?
.abs(),
)?;
let mut opt_minus = option.clone();
opt_minus.underlying_price = Positive::new_decimal(
bump_down(
option.underlying_price.to_dec(),
"greeks::numerical::spot_down",
)?
.abs(),
)?;
let p_plus = price_option_with(&opt_plus, &ClosedFormEngine::ClosedFormBS)?;
let p_minus = price_option_with(&opt_minus, &ClosedFormEngine::ClosedFormBS)?;
let diff = d_sub(
p_plus.to_dec(),
p_minus.to_dec(),
"greeks::numerical::delta::diff",
)?;
Ok(d_div(diff, TWO_H, "greeks::numerical::delta::scaled")?)
}
pub fn numerical_gamma(option: &Options) -> Result<Decimal, GreeksError> {
let mut opt_plus = option.clone();
opt_plus.underlying_price = Positive::new_decimal(
bump_up(
option.underlying_price.to_dec(),
"greeks::numerical::spot_up",
)?
.abs(),
)?;
let mut opt_minus = option.clone();
opt_minus.underlying_price = Positive::new_decimal(
bump_down(
option.underlying_price.to_dec(),
"greeks::numerical::spot_down",
)?
.abs(),
)?;
let p_plus = price_option_with(&opt_plus, &ClosedFormEngine::ClosedFormBS)?;
let p_minus = price_option_with(&opt_minus, &ClosedFormEngine::ClosedFormBS)?;
let p = price_option_with(option, &ClosedFormEngine::ClosedFormBS)?;
let two_p = d_mul(dec!(2.0), p.to_dec(), "greeks::numerical::gamma::two_p")?;
let step = d_sub(p_plus.to_dec(), two_p, "greeks::numerical::gamma::step")?;
let numer = d_add(step, p_minus.to_dec(), "greeks::numerical::gamma::numer")?;
let h_squared = d_mul(H, H, "greeks::numerical::gamma::h_squared")?;
Ok(d_div(numer, h_squared, "greeks::numerical::gamma::scaled")?)
}
pub fn numerical_vega(option: &Options) -> Result<Decimal, GreeksError> {
let mut opt_plus = option.clone();
opt_plus.implied_volatility = Positive::new_decimal(
bump_up(
option.implied_volatility.to_dec(),
"greeks::numerical::vol_up",
)?
.abs(),
)?;
if option.implied_volatility.to_dec() <= H {
let p_plus = price_option_with(&opt_plus, &ClosedFormEngine::ClosedFormBS)?;
let p = price_option_with(option, &ClosedFormEngine::ClosedFormBS)?;
let diff = d_sub(
p_plus.to_dec(),
p.to_dec(),
"greeks::numerical::vega::forward_diff",
)?;
return Ok(d_div(diff, H, "greeks::numerical::vega::forward_scaled")?);
}
let mut opt_minus = option.clone();
opt_minus.implied_volatility = Positive::new_decimal(
bump_down(
option.implied_volatility.to_dec(),
"greeks::numerical::vol_down",
)?
.abs(),
)?;
let p_plus = price_option_with(&opt_plus, &ClosedFormEngine::ClosedFormBS)?;
let p_minus = price_option_with(&opt_minus, &ClosedFormEngine::ClosedFormBS)?;
let diff = d_sub(
p_plus.to_dec(),
p_minus.to_dec(),
"greeks::numerical::vega::diff",
)?;
Ok(d_div(diff, TWO_H, "greeks::numerical::vega::scaled")?)
}
pub fn numerical_theta(option: &Options) -> Result<Decimal, GreeksError> {
let days = option.expiration_date.get_days()?.to_dec();
if days.is_zero() {
return Ok(Decimal::ZERO);
}
let longer = with_days_to_expiry(
option,
d_add(days, THETA_BUMP_DAYS, "greeks::numerical::theta::days_up")?,
)?;
let p_longer = price_option_with(&longer, &ClosedFormEngine::ClosedFormBS)?;
if days > THETA_BUMP_DAYS {
let shorter = with_days_to_expiry(
option,
d_sub(days, THETA_BUMP_DAYS, "greeks::numerical::theta::days_down")?,
)?;
let p_shorter = price_option_with(&shorter, &ClosedFormEngine::ClosedFormBS)?;
let diff = d_sub(
p_shorter.to_dec(),
p_longer.to_dec(),
"greeks::numerical::theta::diff",
)?;
Ok(d_div(
diff,
THETA_SPAN_DAYS,
"greeks::numerical::theta::scaled",
)?)
} else {
let p = price_option_with(option, &ClosedFormEngine::ClosedFormBS)?;
Ok(d_sub(
p.to_dec(),
p_longer.to_dec(),
"greeks::numerical::theta::forward_diff",
)?)
}
}
fn with_days_to_expiry(option: &Options, days: Decimal) -> Result<Options, GreeksError> {
let mut bumped = option.clone();
bumped.expiration_date = ExpirationDate::Days(Positive::new_decimal(days)?);
Ok(bumped)
}
pub fn numerical_rho(option: &Options) -> Result<Decimal, GreeksError> {
let mut opt_plus = option.clone();
opt_plus.risk_free_rate = bump_up(option.risk_free_rate, "greeks::numerical::rate_up")?;
let mut opt_minus = option.clone();
opt_minus.risk_free_rate = bump_down(option.risk_free_rate, "greeks::numerical::rate_down")?;
let p_plus = price_option_with(&opt_plus, &ClosedFormEngine::ClosedFormBS)?;
let p_minus = price_option_with(&opt_minus, &ClosedFormEngine::ClosedFormBS)?;
let diff = d_sub(
p_plus.to_dec(),
p_minus.to_dec(),
"greeks::numerical::rho::diff",
)?;
Ok(d_div(diff, TWO_H, "greeks::numerical::rho::scaled")?)
}
#[cfg(test)]
mod tests {
use super::*;
use optionstratlib_core::model::types::{OptionStyle, Side};
use optionstratlib_core::model::utils::create_sample_option_simplest;
#[test]
fn test_two_h_is_the_product_it_replaced() {
assert_eq!(TWO_H.to_string(), (dec!(2.0) * H).to_string());
}
#[test]
fn test_bumps_at_the_ends_of_the_range_return() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.underlying_price = Positive::MAX;
let _ = numerical_delta(&option);
let _ = numerical_gamma(&option);
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.implied_volatility = Positive::MAX;
let _ = numerical_vega(&option);
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
for rate in [Decimal::MAX, Decimal::MIN] {
option.risk_free_rate = rate;
let _ = numerical_rho(&option);
}
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.expiration_date = ExpirationDate::Days(Positive::MAX);
let _ = numerical_theta(&option);
}
#[test]
fn test_numerical_vega_below_one_vol_point_matches_closed_form() {
let mut option = create_sample_option_simplest(OptionStyle::Call, Side::Long);
option.strike_price = option.underlying_price;
option.risk_free_rate = Decimal::ZERO;
option.dividend_yield = Positive::ZERO;
option.implied_volatility = Positive::new_decimal(dec!(0.005)).unwrap_or(Positive::ONE);
let numerical = numerical_vega(&option).unwrap_or(Decimal::ZERO);
let closed = crate::greeks::vega(&option).unwrap_or(Decimal::ZERO) * dec!(100);
assert!(closed > Decimal::ZERO);
assert!(
(numerical - closed).abs() < closed * dec!(0.01),
"numerical {numerical} vs closed form {closed}"
);
}
#[test]
fn test_bump_matches_the_operator() {
let x = dec!(100.5);
assert_eq!(bump_up(x, "test").ok(), Some(x + H));
assert_eq!(bump_down(x, "test").ok(), Some(x - H));
}
}