alpaca-facade 0.24.9

High-level convenience facades built on top of the alpaca-rust workspace crates
Documentation
use alpaca_facade::OptionChainRequest;
use alpaca_option::OptionRight;
use rust_decimal::Decimal;

#[test]
fn request_from_dte_range_builds_ny_date_window_and_rounded_strikes() {
    let request = OptionChainRequest::from_dte_range(7, 21, Some(432.126), Some(441.874));

    assert_eq!(request.strike_price_gte(), Some(Decimal::new(43213, 2)));
    assert_eq!(request.strike_price_lte(), Some(Decimal::new(44187, 2)));
    assert!(
        request
            .expiration_date_gte()
            .is_some_and(|value| value.len() == 10)
    );
    assert!(
        request
            .expiration_date_lte()
            .is_some_and(|value| value.len() == 10)
    );
}

#[test]
fn request_from_expiration_range_keeps_exact_dates() {
    let request = OptionChainRequest::from_expiration_range(Some("2026-05-15"), Some("2026-06-19"));

    assert_eq!(request.expiration_date_gte(), Some("2026-05-15"));
    assert_eq!(request.expiration_date_lte(), Some("2026-06-19"));
}

#[test]
fn request_with_strike_range_rounds_float_inputs() {
    let request = OptionChainRequest::new().with_strike_range(Some(430.126), Some(441.874));

    assert_eq!(request.strike_price_gte(), Some(Decimal::new(43013, 2)));
    assert_eq!(request.strike_price_lte(), Some(Decimal::new(44187, 2)));
}

#[test]
fn request_with_option_type_sets_right() {
    let request = OptionChainRequest::new().with_option_type(OptionRight::Put);

    assert_eq!(request.option_type(), Some(&OptionRight::Put));
}

#[test]
fn request_with_underlying_price_only_fills_valid_positive_values() {
    let request = OptionChainRequest::new().with_underlying_price(Some(512.25));
    assert_eq!(request.underlying_price(), Some(512.25));

    let unchanged = request.clone().with_underlying_price(Some(0.0));
    assert_eq!(unchanged.underlying_price(), Some(512.25));
}

#[test]
fn request_covers_narrower_window_and_same_option_type() {
    let cached = OptionChainRequest::from_expiration_range(Some("2026-04-20"), Some("2026-05-16"))
        .with_option_type(OptionRight::Call)
        .with_strike_range(Some(90.0), Some(110.0))
        .with_underlying_price(Some(100.0));
    let requested =
        OptionChainRequest::from_expiration_range(Some("2026-04-24"), Some("2026-05-09"))
            .with_option_type(OptionRight::Call)
            .with_strike_range(Some(95.0), Some(105.0))
            .with_underlying_price(Some(101.5));

    assert!(cached.covers(&requested));
    assert!(!requested.covers(&cached));
}

#[test]
fn request_merge_expands_bounds_and_promotes_option_type_to_all_when_needed() {
    let mut merged =
        OptionChainRequest::from_expiration_range(Some("2026-04-24"), Some("2026-05-09"))
            .with_option_type(OptionRight::Call)
            .with_strike_range(Some(95.0), Some(105.0))
            .with_underlying_price(Some(101.5));

    merged.merge(
        &OptionChainRequest::from_expiration_range(Some("2026-04-20"), Some("2026-05-16"))
            .with_option_type(OptionRight::Put)
            .with_strike_range(Some(90.0), Some(110.0))
            .with_underlying_price(Some(99.8)),
    );

    assert_eq!(merged.option_type(), None);
    assert_eq!(merged.strike_price_gte(), Some(Decimal::new(9000, 2)));
    assert_eq!(merged.strike_price_lte(), Some(Decimal::new(11000, 2)));
    assert_eq!(merged.expiration_date_gte(), Some("2026-04-20"));
    assert_eq!(merged.expiration_date_lte(), Some("2026-05-16"));
    assert_eq!(merged.underlying_price(), Some(101.5));
}