libitofin 0.35.0

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

fn close(actual: &[Real], expected: &[Real], tolerance: Real) {
    assert_eq!(actual.len(), expected.len());
    for (&actual, &expected) in actual.iter().zip(expected) {
        assert!(
            (actual - expected).abs() <= tolerance * expected.abs().max(1.0),
            "actual {actual}, expected {expected}"
        );
    }
}

pub(super) fn weighted() -> SequenceStatistics {
    let mut statistics = SequenceStatistics::new(2).unwrap();
    for (row, weight) in [
        ([1.0, 2.0], 1.0),
        ([3.0, 6.0], 2.0),
        ([5.0, 10.0], 1.0),
        ([-7.0, 20.0], 0.0),
    ] {
        statistics.add_weighted(&row, weight).unwrap();
    }
    statistics
}

#[test]
fn weighted_hand_calculation_uses_count_correction() {
    let statistics = weighted();
    assert_eq!(statistics.samples(), 4);
    assert_eq!(statistics.weight_sum(), 4.0);
    close(&statistics.mean().unwrap(), &[3.0, 6.0], 1e-15);
    close(
        &statistics.variance().unwrap(),
        &[8.0 / 3.0, 32.0 / 3.0],
        1e-15,
    );
    close(
        &statistics.covariance().unwrap(),
        &[8.0 / 3.0, 16.0 / 3.0, 16.0 / 3.0, 32.0 / 3.0],
        1e-15,
    );
    close(&statistics.correlation().unwrap(), &[1.0; 4], 1e-15);
    close(
        &statistics.standard_deviation().unwrap(),
        &[(8.0_f64 / 3.0).sqrt(), (32.0_f64 / 3.0).sqrt()],
        1e-15,
    );
    close(
        &statistics.error_estimate().unwrap(),
        &[(2.0_f64 / 3.0).sqrt(), (8.0_f64 / 3.0).sqrt()],
        1e-15,
    );
    assert_eq!(statistics.min().unwrap(), [-7.0, 2.0]);
    assert_eq!(statistics.max().unwrap(), [5.0, 20.0]);
}

#[test]
fn batch_rows_and_matrix_outputs_are_row_major() {
    let values = [1.0, 10.0, 5.0, 30.0, 7.0, -10.0];
    close(
        &evaluate_sequence_batch(&values, 3, 2, None, SequenceStatistic::Mean).unwrap(),
        &[13.0 / 3.0, 10.0],
        1e-15,
    );
    close(
        &evaluate_sequence_batch(&values, 3, 2, None, SequenceStatistic::Covariance).unwrap(),
        &[28.0 / 3.0, -20.0, -20.0, 400.0],
        1e-15,
    );
    assert_eq!(
        evaluate_sequence_batch(&values, 3, 2, None, SequenceStatistic::Minimum).unwrap(),
        [1.0, -10.0]
    );
    assert_eq!(
        evaluate_sequence_batch(&values, 3, 2, None, SequenceStatistic::Maximum).unwrap(),
        [7.0, 30.0]
    );
}

#[test]
fn all_batch_measures_match_accumulator() {
    let samples = [1.0, 2.0, 3.0, 6.0, 5.0, 10.0, -7.0, 20.0];
    let weights = [1.0, 2.0, 1.0, 0.0];
    let statistics = weighted();
    for (measure, expected) in [
        (SequenceStatistic::Mean, statistics.mean()),
        (SequenceStatistic::Variance, statistics.variance()),
        (
            SequenceStatistic::StandardDeviation,
            statistics.standard_deviation(),
        ),
        (
            SequenceStatistic::ErrorEstimate,
            statistics.error_estimate(),
        ),
        (SequenceStatistic::Minimum, statistics.min()),
        (SequenceStatistic::Maximum, statistics.max()),
        (SequenceStatistic::Covariance, statistics.covariance()),
        (SequenceStatistic::Correlation, statistics.correlation()),
    ] {
        assert_eq!(
            evaluate_sequence_batch(&samples, 4, 2, Some(&weights), measure).unwrap(),
            expected.unwrap()
        );
    }
}

#[test]
fn covariance_is_exactly_symmetric_and_matches_variance_diagonal() {
    let mut statistics = SequenceStatistics::new(3).unwrap();
    for row in [[1.2, -3.0, 8.0], [7.1, 8.0, -1.0], [2.0, 4.0, 3.1]] {
        statistics.add(&row).unwrap();
    }
    let covariance = statistics.covariance().unwrap();
    let variance = statistics.variance().unwrap();
    for i in 0..3 {
        assert_eq!(covariance[i * 3 + i], variance[i]);
        for j in 0..3 {
            assert_eq!(covariance[i * 3 + j], covariance[j * 3 + i]);
        }
    }
    let correlation = statistics.correlation().unwrap();
    for i in 0..3 {
        assert_eq!(correlation[i * 3 + i], 1.0);
        for j in 0..3 {
            assert_eq!(correlation[i * 3 + j], correlation[j * 3 + i]);
        }
    }
}

#[test]
fn constant_components_use_native_correlation_convention() {
    let mut statistics = SequenceStatistics::new(3).unwrap();
    for row in [[7.0, -2.0, 1.0], [7.0, -2.0, 2.0], [7.0, -2.0, 3.0]] {
        statistics.add(&row).unwrap();
    }
    close(
        &statistics.covariance().unwrap(),
        &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0],
        1e-15,
    );
    assert_eq!(
        statistics.correlation().unwrap(),
        [1.0, 1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0]
    );
}

#[test]
fn large_offset_and_near_constant_variance_remain_centered() {
    let base = 1e16;
    let mut statistics = SequenceStatistics::new(2).unwrap();
    for offset in [0.0, 2.0, 4.0] {
        statistics
            .add(&[base + offset, -base - 2.0 * offset])
            .unwrap();
    }
    assert_eq!(statistics.mean().unwrap(), [base + 2.0, -base - 4.0]);
    close(&statistics.variance().unwrap(), &[4.0, 16.0], 1e-15);
    close(
        &statistics.covariance().unwrap(),
        &[4.0, -8.0, -8.0, 16.0],
        1e-15,
    );
    close(
        &statistics.correlation().unwrap(),
        &[1.0, -1.0, -1.0, 1.0],
        1e-15,
    );
    let mut tiny = SequenceStatistics::new(1).unwrap();
    for value in [1.0, 1.0 + 1e-12, 1.0 - 1e-12] {
        tiny.add(&[value]).unwrap();
    }
    assert!(tiny.variance().unwrap()[0] > 1e-25);
}

#[test]
fn large_weights_are_normalized_before_sample_products() {
    let mut statistics = SequenceStatistics::new(1).unwrap();
    statistics.add_weighted(&[1e100], 1e300).unwrap();
    statistics.add_weighted(&[1e100 + 1e90], 1e300).unwrap();
    assert!(statistics.mean().unwrap()[0].is_finite());
    assert!(statistics.variance().unwrap()[0].is_finite());
    let difference = (1e100 + 1e90) - 1e100;
    close(
        &statistics.variance().unwrap(),
        &[difference * difference / 2.0],
        1e-15,
    );
}

#[test]
fn tiny_positive_weight_can_contribute_finite_extreme_moments() {
    let mut statistics = SequenceStatistics::new(1).unwrap();
    statistics.add_weighted(&[0.0], 1e300).unwrap();
    statistics.add_weighted(&[1e300], 1e-300).unwrap();
    close(&statistics.mean().unwrap(), &[1e-300], 1e-15);
    close(&statistics.variance().unwrap(), &[2.0], 1e-15);
    let mut tiny = SequenceStatistics::new(1).unwrap();
    tiny.add_weighted(&[1.0], Real::from_bits(1)).unwrap();
    tiny.add_weighted(&[3.0], Real::from_bits(1)).unwrap();
    close(&tiny.mean().unwrap(), &[2.0], 1e-15);
    close(&tiny.variance().unwrap(), &[2.0], 1e-15);
}

#[test]
fn opposite_large_values_mean_is_finite_but_unrepresentable_variance_errors() {
    let mut statistics = SequenceStatistics::new(1).unwrap();
    statistics.add_weighted(&[Real::MAX], 1e-300).unwrap();
    statistics.add_weighted(&[-Real::MAX], 1e-300).unwrap();
    assert_eq!(statistics.mean().unwrap(), [0.0]);
    assert!(statistics.variance().is_err());
    assert!(statistics.covariance().is_err());
    assert!(statistics.correlation().is_err());
}

#[test]
fn huge_constants_and_zero_weight_extremes_do_not_overflow() {
    let mut statistics = SequenceStatistics::new(2).unwrap();
    statistics
        .add_weighted(&[Real::MAX, Real::MAX], 1e300)
        .unwrap();
    statistics
        .add_weighted(&[Real::MAX, Real::MAX], 1e300)
        .unwrap();
    statistics
        .add_weighted(&[-Real::MAX, -Real::MAX], 0.0)
        .unwrap();
    assert_eq!(statistics.mean().unwrap(), [Real::MAX; 2]);
    assert_eq!(statistics.variance().unwrap(), [0.0; 2]);
    assert_eq!(statistics.covariance().unwrap(), [0.0; 4]);
    assert_eq!(statistics.correlation().unwrap(), [1.0; 4]);
    assert_eq!(statistics.min().unwrap(), [-Real::MAX; 2]);
}

#[test]
fn correlation_avoids_variance_product_overflow() {
    let mut statistics = SequenceStatistics::new(2).unwrap();
    statistics.add(&[1e100, -1e100]).unwrap();
    statistics.add(&[2e100, -2e100]).unwrap();
    close(
        &statistics.correlation().unwrap(),
        &[1.0, -1.0, -1.0, 1.0],
        1e-15,
    );
}

#[test]
fn dominant_weight_center_prevents_mean_cancellation_in_both_row_orders() {
    for (values, weights, expected) in [
        ([1e16, 1.0], [1e-16, 1.0], 2.0),
        ([1e300, 0.0], [1e-300, 1e300], 1e-300),
    ] {
        for reverse in [false, true] {
            let mut statistics = SequenceStatistics::new(1).unwrap();
            let indices = if reverse { [1, 0] } else { [0, 1] };
            for index in indices {
                statistics
                    .add_weighted(&[values[index]], weights[index])
                    .unwrap();
            }
            assert!((statistics.mean().unwrap()[0] / expected - 1.0).abs() < 1e-15);
            assert!(statistics.variance().unwrap()[0].is_finite());
        }
    }
}

#[test]
fn cancellation_of_opposing_samples_retains_small_mean() {
    for values in [
        [1e16, 1.0, -1e16],
        [-1e16, 1.0, 1e16],
        [1.0, 1e16, -1e16],
        [1e16, -1e16, 1.0],
        [-1e16, 1e16, 1.0],
        [1.0, -1e16, 1e16],
    ] {
        close(
            &evaluate_sequence_batch(&values, 3, 1, None, SequenceStatistic::Mean).unwrap(),
            &[1.0 / 3.0],
            1e-15,
        );
    }
}