alpaca-option 0.24.9

Provider-neutral option semantics and math for the alpaca-rust workspace
Documentation
use alpaca_time::clock;
use alpaca_time::range;

use crate::error::{OptionError, OptionResult};

fn canonical_date(date: &str) -> OptionResult<String> {
    clock::parse_date(date).map_err(|_| {
        OptionError::new(
            "invalid_expiration_selection_input",
            format!("invalid date: {date}"),
        )
    })
}

pub fn nearest_weekly_expiration(anchor_date: &str) -> OptionResult<String> {
    let anchor = canonical_date(anchor_date)?;
    let end = range::add_days(&anchor, 14).map_err(|error| {
        OptionError::new("invalid_expiration_selection_input", error.to_string())
    })?;
    let candidates = range::weekly_last_trading_dates(&anchor, &end).map_err(|error| {
        OptionError::new("invalid_expiration_selection_input", error.to_string())
    })?;
    candidates.into_iter().next().ok_or_else(|| {
        OptionError::new(
            "missing_weekly_expiration",
            format!("no weekly expiration found on or after {anchor_date}"),
        )
    })
}

pub fn weekly_expirations_between(start_date: &str, end_date: &str) -> OptionResult<Vec<String>> {
    let start_date = canonical_date(start_date)?;
    let end_date = canonical_date(end_date)?;
    range::weekly_last_trading_dates(&start_date, &end_date)
        .map_err(|error| OptionError::new("invalid_expiration_selection_input", error.to_string()))
}

pub fn standard_monthly_expiration(year: i32, month: u32) -> OptionResult<String> {
    let third_friday = range::nth_weekday(year, month, "fri", 3).map_err(|error| {
        OptionError::new("invalid_expiration_selection_input", error.to_string())
    })?;
    range::last_trading_date_of_week(&third_friday)
        .map_err(|error| OptionError::new("invalid_expiration_selection_input", error.to_string()))
}