libitofin 0.52.1

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

fn close(actual: f64, expected: f64) {
    assert!(
        (actual - expected).abs() <= 2e-14 * expected.abs().max(1.0),
        "{actual} != {expected}"
    );
}

#[test]
fn independently_calculated_cumulative_fixture() {
    let price = [10.0, 12.0, 11.0, 11.0, 9.0];
    let volume = [0.0, 2.0, 1.0, 0.0, 3.0];
    let result = vwap(&price, &volume).unwrap();
    assert_eq!(result.first_valid, 1);
    for (&actual, expected) in
        result
            .values
            .iter()
            .zip([0.0, 12.0, 35.0 / 3.0, 35.0 / 3.0, 31.0 / 3.0])
    {
        close(actual, expected);
    }
    assert_eq!(result.get(0), None);
    assert_eq!(result.get(1), Some(12.0));
    assert_eq!(result.get(price.len()), None);
    assert_eq!(
        obv(&price, &volume).unwrap().values,
        [0.0, 2.0, 1.0, 1.0, -2.0]
    );
}

#[test]
fn vwap_is_cumulative_not_rolling_and_session_reset_is_explicit() {
    let result = vwap(&[2.0, 4.0, 8.0], &[1.0, 1.0, 2.0]).unwrap();
    assert_eq!(result.values, [2.0, 3.0, 5.5]);
    assert_eq!(result.first_valid, 0);
    let reset = vwap(&[8.0], &[2.0]).unwrap();
    assert_eq!(reset.values, [8.0]);
    assert_eq!(reset.first_valid, 0);
}

#[test]
fn vwap_preserves_negative_and_zero_caller_prices() {
    let result = vwap(&[-3.0, -1.0, 1.0], &[1.0, 1.0, 1.0]).unwrap();
    for (&actual, expected) in result.values.iter().zip([-3.0, -2.0, -1.0]) {
        close(actual, expected);
    }
    assert_eq!(vwap(&[0.0, 0.0], &[1.0, 2.0]).unwrap().values, [0.0, 0.0]);
}

#[test]
fn missing_vwap_prefix_and_zero_volume_carry_align() {
    let result = vwap(&[100.0, 200.0, 3.0, 999.0], &[0.0, -0.0, 2.0, 0.0]).unwrap();
    assert_eq!(result.first_valid, 2);
    assert_eq!(result.values, [0.0, 0.0, 3.0, 3.0]);
    assert_eq!(result.get(1), None);
    assert_eq!(result.get(2), Some(3.0));
    let missing = vwap(&[1.0, -1.0], &[0.0, 0.0]).unwrap();
    assert_eq!(missing.first_valid, 2);
    assert_eq!(missing.values, [0.0, 0.0]);
    assert_eq!(missing.get(0), None);
    assert_eq!(missing.get(1), None);
}

#[test]
fn empty_and_single_bar_contracts() {
    for result in [vwap(&[], &[]).unwrap(), obv(&[], &[]).unwrap()] {
        assert!(result.values.is_empty());
        assert_eq!(result.first_valid, 0);
        assert_eq!(result.get(0), None);
    }
    let single = vwap(&[7.0], &[3.0]).unwrap();
    assert_eq!(single.values, [7.0]);
    assert_eq!(single.first_valid, 0);
    assert_eq!(obv(&[7.0], &[3.0]).unwrap().values, [0.0]);
    let missing = vwap(&[7.0], &[0.0]).unwrap();
    assert_eq!(missing.first_valid, 1);
    assert_eq!(missing.get(0), None);
}

#[test]
fn obv_zero_seed_equal_closes_and_zero_volumes() {
    let result = obv(&[10.0, 12.0, 12.0, 8.0, 9.0], &[999.0, 2.0, 50.0, 3.0, 0.0]).unwrap();
    assert_eq!(result.values, [0.0, 2.0, 2.0, -1.0, -1.0]);
    assert_eq!(result.first_valid, 0);
    assert_eq!(result.get(0), Some(0.0));
    assert_eq!(
        obv(&[-3.0, -1.0, 1.0], &[1.0, 1.0, 1.0]).unwrap().values,
        [0.0, 1.0, 2.0]
    );
    assert_eq!(
        obv(&[1.0, 2.0, 3.0], &[0.0, 0.0, -0.0]).unwrap().values,
        [0.0, 0.0, 0.0]
    );
}

#[test]
fn invalid_lengths_and_every_nonfinite_position_are_rejected() {
    for function in [vwap, obv] {
        assert!(function(&[], &[0.0]).is_err());
        assert!(function(&[1.0], &[]).is_err());
        assert!(function(&[1.0, 2.0], &[1.0]).is_err());
        for index in 0..3 {
            for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
                let mut price = [1.0, 2.0, 3.0];
                price[index] = invalid;
                assert!(function(&price, &[0.0, 0.0, 0.0]).is_err());
                let mut volume = [0.0, 0.0, 0.0];
                volume[index] = invalid;
                assert!(function(&[1.0, 2.0, 3.0], &volume).is_err());
            }
            let mut volume = [0.0, 0.0, 0.0];
            volume[index] = -1.0;
            assert!(function(&[1.0, 2.0, 3.0], &volume).is_err());
        }
    }
}

#[test]
fn vwap_avoids_price_volume_product_overflow() {
    let result = vwap(&[f64::MAX, f64::MAX], &[2.0, 2.0]).unwrap();
    assert_eq!(result.values, [f64::MAX, f64::MAX]);
    let result = vwap(&[-f64::MAX, -f64::MAX], &[2.0, 2.0]).unwrap();
    assert_eq!(result.values, [-f64::MAX, -f64::MAX]);
    assert_eq!(vwap(&[f64::MAX], &[f64::MAX]).unwrap().values, [f64::MAX]);
}

#[test]
fn opposite_signed_extreme_prices_have_finite_convex_means() {
    let result = vwap(&[-f64::MAX, f64::MAX, -f64::MAX], &[1.0, 1.0, 2.0]).unwrap();
    assert_eq!(result.values, [-f64::MAX, 0.0, -f64::MAX * 0.5]);
    let result = vwap(&[f64::MAX, -f64::MAX], &[1.0, 3.0]).unwrap();
    close(result.values[1] / f64::MAX, -0.5);
    assert_eq!(
        obv(&[-f64::MAX, f64::MAX, -f64::MAX], &[1.0, 2.0, 3.0])
            .unwrap()
            .values,
        [0.0, 2.0, -1.0]
    );
}

#[test]
fn subnormal_volumes_have_exact_initial_prices_and_nonzero_weight() {
    let tiny = f64::from_bits(1);
    let result = vwap(&[3.0, 5.0], &[tiny, tiny]).unwrap();
    assert_eq!(result.values, [3.0, 4.0]);
    assert_eq!(vwap(&[tiny], &[tiny]).unwrap().values, [tiny]);
    let result = vwap(&[0.0, -f64::MAX], &[0.0, tiny]).unwrap();
    assert_eq!(result.first_valid, 1);
    assert_eq!(result.values, [0.0, -f64::MAX]);
}

#[test]
fn volume_and_obv_overflow_are_errors_with_bar_index() {
    let error = vwap(&[0.0, 0.0], &[f64::MAX, f64::MAX]).unwrap_err();
    assert!(error.message().contains("cumulative volume at index 1"));
    let error = obv(&[1.0, 2.0, 3.0], &[0.0, f64::MAX, f64::MAX]).unwrap_err();
    assert!(error.message().contains("OBV at index 2"));
    let error = obv(&[3.0, 2.0, 1.0], &[0.0, f64::MAX, f64::MAX]).unwrap_err();
    assert!(error.message().contains("OBV at index 2"));
}

#[test]
fn obv_does_not_sum_unsigned_volumes_or_initial_seed() {
    let result = obv(&[1.0, 1.0, 1.0], &[f64::MAX; 3]).unwrap();
    assert_eq!(result.values, [0.0, 0.0, 0.0]);
    let result = obv(&[1.0, 2.0, 1.0, 2.0], &[f64::MAX; 4]).unwrap();
    assert_eq!(result.values, [0.0, f64::MAX, 0.0, f64::MAX]);
}

#[test]
fn independent_weight_ratios_preserve_dominant_new_volume_tail() {
    let result = vwap(&[f64::MAX, 0.0], &[1.0, f64::MAX]).unwrap();
    close(result.values[1], 1.0);
    let result = vwap(&[-f64::MAX, 0.0], &[1.0, f64::MAX]).unwrap();
    close(result.values[1], -1.0);
    let result = vwap(&[f64::MAX, -1.0], &[1.0, f64::MAX]).unwrap();
    close(result.values[1], 0.0);
    let result = vwap(&[-f64::MAX, 1.0], &[1.0, f64::MAX]).unwrap();
    close(result.values[1], 0.0);
}

#[test]
fn same_sign_convex_mean_anchors_on_dominant_weight() {
    let low_weight = vwap(&[4.0, 8.0], &[3.0, 1.0]).unwrap();
    let high_weight = vwap(&[4.0, 8.0], &[1.0, 3.0]).unwrap();
    assert_eq!(low_weight.values, [4.0, 5.0]);
    assert_eq!(high_weight.values, [4.0, 7.0]);
    assert_eq!(
        vwap(&[-4.0, -8.0], &[1.0, 3.0]).unwrap().values,
        [-4.0, -7.0]
    );
    let extremes = vwap(&[f64::MAX, 0.0], &[1.0, 3.0]).unwrap();
    assert_eq!(extremes.values[1], f64::MAX * 0.25);
}