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::{brent_solve, normal_cdf, normal_pdf};
use crate::types::{
    BlackScholesImpliedVolatilityInput, BlackScholesInput, Greeks, OptionContract, OptionRight,
};

fn ensure_finite(name: &str, value: f64) -> OptionResult<()> {
    if value.is_finite() {
        Ok(())
    } else {
        Err(OptionError::new(
            "invalid_pricing_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_pricing_input",
            format!("{name} must be greater than zero: {value}"),
        ))
    }
}

fn parse_option_right(option_right: &str) -> OptionResult<OptionRight> {
    OptionRight::from_str(option_right).map_err(|_| {
        OptionError::new(
            "invalid_pricing_input",
            format!("invalid option right: {option_right}"),
        )
    })
}

fn validate_black_scholes_inputs(input: &BlackScholesInput) -> OptionResult<()> {
    ensure_positive("spot", input.spot)?;
    ensure_positive("strike", input.strike)?;
    ensure_positive("years", input.years)?;
    ensure_finite("rate", input.rate)?;
    ensure_finite("dividend_yield", input.dividend_yield)?;
    ensure_positive("volatility", input.volatility)?;
    Ok(())
}

fn d1_d2(input: &BlackScholesInput) -> (f64, f64) {
    let sqrt_years = input.years.sqrt();
    let sigma_sqrt_t = input.volatility * sqrt_years;
    let d1 = ((input.spot / input.strike).ln()
        + (input.rate - input.dividend_yield + 0.5 * input.volatility * input.volatility)
            * input.years)
        / sigma_sqrt_t;
    let d2 = d1 - sigma_sqrt_t;
    (d1, d2)
}

fn price_black_scholes_core(input: &BlackScholesInput) -> f64 {
    let (d1, d2) = d1_d2(input);
    let discount_spot = (-input.dividend_yield * input.years).exp();
    let discount_strike = (-input.rate * input.years).exp();

    match input.option_right {
        OptionRight::Call => {
            input.spot * discount_spot * normal_cdf(d1)
                - input.strike * discount_strike * normal_cdf(d2)
        }
        OptionRight::Put => {
            input.strike * discount_strike * normal_cdf(-d2)
                - input.spot * discount_spot * normal_cdf(-d1)
        }
    }
}

fn discounted_forward_minus_strike(input: &BlackScholesInput) -> f64 {
    input.spot * (-input.dividend_yield * input.years).exp()
        - input.strike * (-input.rate * input.years).exp()
}

fn european_no_arbitrage_lower_bound(input: &BlackScholesInput) -> f64 {
    let parity = discounted_forward_minus_strike(input);
    match input.option_right {
        OptionRight::Call => parity.max(0.0),
        OptionRight::Put => (-parity).max(0.0),
    }
}

pub fn price_black_scholes(input: &BlackScholesInput) -> OptionResult<f64> {
    validate_black_scholes_inputs(input)?;
    Ok(price_black_scholes_core(input))
}

pub fn greeks_black_scholes(input: &BlackScholesInput) -> OptionResult<Greeks> {
    validate_black_scholes_inputs(input)?;
    let (d1, d2) = d1_d2(input);
    let sqrt_years = input.years.sqrt();
    let sigma_sqrt_t = input.volatility * sqrt_years;
    let exp_minus_qt = (-input.dividend_yield * input.years).exp();
    let exp_minus_rt = (-input.rate * input.years).exp();
    let nd1 = normal_cdf(d1);
    let nd2 = normal_cdf(d2);
    let n_minus_d1 = normal_cdf(-d1);
    let n_minus_d2 = normal_cdf(-d2);
    let phi_d1 = normal_pdf(d1);

    let delta = match input.option_right {
        OptionRight::Call => exp_minus_qt * nd1,
        OptionRight::Put => -exp_minus_qt * n_minus_d1,
    };
    let gamma = exp_minus_qt * phi_d1 / (input.spot * sigma_sqrt_t);
    let vega = input.spot * exp_minus_qt * phi_d1 * sqrt_years / 100.0;
    let theta_annual = match input.option_right {
        OptionRight::Call => {
            -input.spot * exp_minus_qt * phi_d1 * input.volatility / (2.0 * sqrt_years)
                - input.rate * input.strike * exp_minus_rt * nd2
                + input.dividend_yield * input.spot * exp_minus_qt * nd1
        }
        OptionRight::Put => {
            -input.spot * exp_minus_qt * phi_d1 * input.volatility / (2.0 * sqrt_years)
                + input.rate * input.strike * exp_minus_rt * n_minus_d2
                - input.dividend_yield * input.spot * exp_minus_qt * n_minus_d1
        }
    };
    let theta = theta_annual / 365.0;
    let rho = match input.option_right {
        OptionRight::Call => input.strike * input.years * exp_minus_rt * nd2,
        OptionRight::Put => -input.strike * input.years * exp_minus_rt * n_minus_d2,
    };

    Ok(Greeks {
        delta,
        gamma,
        vega,
        theta,
        rho,
    })
}

pub fn intrinsic_value(spot: f64, strike: f64, option_right: &str) -> OptionResult<f64> {
    ensure_finite("spot", spot)?;
    ensure_positive("strike", strike)?;
    let option_right = parse_option_right(option_right)?;

    Ok(match option_right {
        OptionRight::Call => (spot - strike).max(0.0),
        OptionRight::Put => (strike - spot).max(0.0),
    })
}

pub fn extrinsic_value(
    option_price: f64,
    spot: f64,
    strike: f64,
    option_right: &str,
) -> OptionResult<f64> {
    ensure_finite("option_price", option_price)?;
    Ok((option_price - intrinsic_value(spot, strike, option_right)?).max(0.0))
}

pub fn contract_extrinsic_value(
    option_price: f64,
    spot: f64,
    contract: &OptionContract,
) -> OptionResult<f64> {
    extrinsic_value(
        option_price,
        spot,
        contract.strike,
        contract.option_right.as_str(),
    )
}

pub fn implied_volatility_from_price(
    input: &BlackScholesImpliedVolatilityInput,
) -> OptionResult<f64> {
    ensure_finite("target_price", input.target_price)?;
    let black_scholes_input = BlackScholesInput {
        spot: input.spot,
        strike: input.strike,
        years: input.years,
        rate: input.rate,
        dividend_yield: input.dividend_yield,
        volatility: 0.2,
        option_right: input.option_right.clone(),
    };
    validate_black_scholes_inputs(&black_scholes_input)?;
    let minimum_price = european_no_arbitrage_lower_bound(&black_scholes_input);
    if input.target_price + 1e-12 < minimum_price {
        return Err(OptionError::new(
            "invalid_pricing_input",
            format!(
                "target_price is below discounted no-arbitrage lower bound: {} < {minimum_price}",
                input.target_price
            ),
        ));
    }

    let lower_bound = input.lower_bound.unwrap_or(0.0001);
    let upper_bound = input.upper_bound.unwrap_or(5.0);
    ensure_positive("lower_bound", lower_bound)?;
    ensure_positive("upper_bound", upper_bound)?;
    if lower_bound >= upper_bound {
        return Err(OptionError::new(
            "invalid_pricing_input",
            format!("lower_bound must be less than upper_bound: {lower_bound} >= {upper_bound}"),
        ));
    }

    brent_solve(
        lower_bound,
        upper_bound,
        |volatility| {
            price_black_scholes_core(&BlackScholesInput {
                volatility,
                ..black_scholes_input.clone()
            }) - input.target_price
        },
        input.tolerance,
        input.max_iterations,
    )
}