alpaca-option 0.24.9

Provider-neutral option semantics and math for the alpaca-rust workspace
Documentation
use crate::error::{OptionError, OptionResult};
use crate::numeric::normal_cdf;

fn ensure_finite(name: &str, value: f64) -> OptionResult<()> {
    if value.is_finite() {
        Ok(())
    } else {
        Err(OptionError::new(
            "invalid_probability_input",
            format!("{name} must be finite: {value}"),
        ))
    }
}

fn ensure_positive(name: &str, value: f64) -> OptionResult<()> {
    ensure_finite(name, value)?;
    if value > 0.0 {
        Ok(())
    } else {
        Err(OptionError::new(
            "invalid_probability_input",
            format!("{name} must be greater than zero: {value}"),
        ))
    }
}

pub fn expiry_probability_in_range(
    spot: f64,
    lower_price: f64,
    upper_price: f64,
    years: f64,
    rate: f64,
    dividend_yield: f64,
    volatility: f64,
) -> OptionResult<f64> {
    ensure_positive("spot", spot)?;
    ensure_positive("lower_price", lower_price)?;
    ensure_positive("upper_price", upper_price)?;
    ensure_positive("years", years)?;
    ensure_finite("rate", rate)?;
    ensure_finite("dividend_yield", dividend_yield)?;
    ensure_positive("volatility", volatility)?;
    if lower_price >= upper_price {
        return Err(OptionError::new(
            "invalid_probability_input",
            format!("lower_price must be less than upper_price: {lower_price} >= {upper_price}"),
        ));
    }

    let sigma_sqrt_t = volatility * years.sqrt();
    let d2_lower = ((spot / lower_price).ln()
        + (rate - dividend_yield - 0.5 * volatility * volatility) * years)
        / sigma_sqrt_t;
    let d2_upper = ((spot / upper_price).ln()
        + (rate - dividend_yield - 0.5 * volatility * volatility) * years)
        / sigma_sqrt_t;

    Ok(normal_cdf(d2_lower) - normal_cdf(d2_upper))
}