libitofin 0.52.1

A ground-up Rust port of QuantLib: quantitative-finance primitives for pricing, risk, and numerical methods.
Documentation
use super::{atr, atr_default, true_range};

fn assert_close(actual: f64, expected: f64) {
    assert!(
        (actual - expected).abs() <= 4.0 * f64::EPSILON * expected.abs().max(1.0),
        "actual {actual}, expected {expected}"
    );
}

#[test]
fn hand_calculated_gaps_and_wilder_recurrence() {
    let high = [12.0, 16.0, 11.0, 15.0, 14.0, 14.0];
    let low = [10.0, 14.0, 9.0, 13.0, 12.0, 14.0];
    let close = [11.0, 15.0, 10.0, 14.0, 13.0, 14.0];
    let ranges = true_range(&high, &low, &close).unwrap();
    assert_eq!(ranges.first_valid, 0);
    assert_eq!(ranges.values, [2.0, 5.0, 6.0, 5.0, 2.0, 1.0]);
    let result = atr(&high, &low, &close, 3).unwrap();
    assert_eq!(result.first_valid, 2);
    assert_eq!(result.values[..2], [0.0, 0.0]);
    assert_eq!(result.get(1), None);
    let expected = [13.0 / 3.0, 41.0 / 9.0, 100.0 / 27.0, 227.0 / 81.0];
    for (actual, expected) in result.values[2..].iter().zip(expected) {
        assert_close(*actual, expected);
    }
}

#[test]
fn default_uses_fourteen_bar_arithmetic_seed() {
    let mut high = [12.0; 15];
    let mut low = [10.0; 15];
    let mut close = [11.0; 15];
    high[14] = 15.0;
    low[14] = 13.0;
    close[14] = 14.0;
    let result = atr_default(&high, &low, &close).unwrap();
    assert_eq!(result, atr(&high, &low, &close, 14).unwrap());
    assert_eq!(result.first_valid, 13);
    assert_eq!(result.values[..13], [0.0; 13]);
    assert_eq!(result.values[13], 2.0);
    assert_close(result.values[14], 15.0 / 7.0);
}

#[test]
fn nonconstant_default_seed_and_next_bar_use_wilder_weights() {
    let high: Vec<f64> = (1..=15).map(f64::from).collect();
    let result = atr_default(&high, &[0.0; 15], &[0.0; 15]).unwrap();
    assert_eq!(result.first_valid, 13);
    assert_eq!(result.values[13], 7.5);
    assert_close(result.values[14], 225.0 / 28.0);
}

#[test]
fn zero_ranges_and_single_bar_follow_first_bar_policy() {
    let result = true_range(&[-2.0; 4], &[-2.0; 4], &[-2.0; 4]).unwrap();
    assert_eq!(result.values, [0.0; 4]);
    assert_eq!(
        atr(&[-2.0; 4], &[-2.0; 4], &[-2.0; 4], 2).unwrap().values,
        [0.0; 4]
    );
    assert_eq!(
        true_range(&[5.0], &[1.0], &[3.0]).unwrap().get(0),
        Some(4.0)
    );
    assert_eq!(atr(&[5.0], &[1.0], &[3.0], 1).unwrap().get(0), Some(4.0));
}

#[test]
fn period_one_preserves_tiny_range_after_a_huge_range() {
    let high = [f64::MAX, f64::from_bits(1)];
    let low = [0.0, 0.0];
    let close = [0.0, 0.0];
    let result = atr(&high, &low, &close, 1).unwrap();
    assert_eq!(result, true_range(&high, &low, &close).unwrap());
    assert_eq!(result.values[1].to_bits(), 1);
}

#[test]
fn empty_and_short_series_are_aligned_missing_values() {
    assert!(true_range(&[], &[], &[]).unwrap().values.is_empty());
    assert_eq!(atr(&[], &[], &[], 14).unwrap().first_valid, 0);
    let result = atr(&[2.0, 3.0], &[0.0, 1.0], &[1.0, 2.0], 3).unwrap();
    assert_eq!(result.first_valid, 2);
    assert_eq!(result.values, [0.0, 0.0]);
    assert_eq!(result.get(0), None);
    assert_eq!(result.get(1), None);
    let huge_period = atr(&[2.0], &[0.0], &[1.0], usize::MAX).unwrap();
    assert_eq!(huge_period.first_valid, 1);
    assert_eq!(huge_period.get(0), None);
}

#[test]
fn seed_and_smoothing_do_not_overflow_a_representable_mean() {
    let result = atr(&[f64::MAX; 5], &[0.0; 5], &[0.0; 5], 3).unwrap();
    assert_eq!(result.values, [0.0, 0.0, f64::MAX, f64::MAX, f64::MAX]);
    let mixed = atr(&[f64::MAX, 0.0, 0.0], &[0.0; 3], &[0.0; 3], 3).unwrap();
    assert_close(mixed.values[2] / f64::MAX, 1.0 / 3.0);
}

#[test]
fn rejects_zero_period_even_with_empty_data() {
    assert!(atr(&[], &[], &[], 0).is_err());
    assert!(atr(&[2.0], &[0.0], &[1.0], 0).is_err());
}

#[test]
fn rejects_every_length_mismatch() {
    for (high, low, close) in [
        (vec![], vec![0.0], vec![1.0]),
        (vec![2.0], vec![], vec![1.0]),
        (vec![2.0], vec![0.0], vec![]),
    ] {
        assert!(true_range(&high, &low, &close).is_err());
        assert!(atr(&high, &low, &close, 14).is_err());
    }
}

#[test]
fn rejects_nonfinite_values_in_every_input_and_warmup() {
    for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
        for column in 0..3 {
            let mut high = [2.0, 2.0];
            let mut low = [0.0, 0.0];
            let mut close = [1.0, 1.0];
            match column {
                0 => high[1] = value,
                1 => low[1] = value,
                _ => close[1] = value,
            }
            assert!(true_range(&high, &low, &close).is_err());
            assert!(atr(&high, &low, &close, 14).is_err());
        }
    }
}

#[test]
fn rejects_unordered_hlc_and_all_overflow_candidates() {
    for (high, low, close) in [
        (vec![0.0], vec![2.0], vec![1.0]),
        (vec![2.0], vec![0.0], vec![3.0]),
        (vec![2.0], vec![0.0], vec![-1.0]),
        (vec![f64::MAX], vec![-f64::MAX], vec![0.0]),
        (
            vec![-f64::MAX, f64::MAX],
            vec![-f64::MAX, 0.0],
            vec![-f64::MAX, 0.0],
        ),
        (
            vec![f64::MAX, 0.0],
            vec![f64::MAX, -f64::MAX],
            vec![f64::MAX, 0.0],
        ),
    ] {
        assert!(true_range(&high, &low, &close).is_err());
        assert!(atr(&high, &low, &close, 14).is_err());
    }
}