libitofin 0.35.0

A ground-up Rust port of QuantLib: quantitative-finance primitives for pricing, risk, and numerical methods.
Documentation
use super::*;
use crate::methods::montecarlo::simulation_kernel::{GbmRequest, gbm_paths};

fn request() -> MertonRequest {
    MertonRequest {
        spot: 100.0,
        drift: 0.05,
        volatility: 0.2,
        jump_intensity: 1.0,
        log_mean_jump: -0.1,
        log_jump_volatility: 0.3,
        horizon: 1.0,
        steps: 4,
        paths: 3,
        seed: 42,
        terminal_only: false,
    }
}

fn assert_close(actual: Real, expected: Real, tolerance: Real) {
    assert!(
        (actual - expected).abs() <= tolerance,
        "actual={actual:.17}, expected={expected:.17}, tolerance={tolerance}"
    );
}

#[test]
fn zero_jump_matches_scalar_gbm_bits_in_both_volatility_branches() {
    for volatility in [0.0, 0.2, 0.731] {
        for seed in [1, 42, Natural::MAX] {
            for terminal_only in [false, true] {
                let r = MertonRequest {
                    jump_intensity: 0.0,
                    volatility,
                    seed,
                    terminal_only,
                    steps: 7,
                    horizon: 1.17,
                    ..request()
                };
                let initial = [r.spot];
                let drift = [r.drift];
                let volatility = [r.volatility];
                let gbm = GbmRequest {
                    initial: &initial,
                    drift: &drift,
                    volatility: &volatility,
                    correlation: &[1.0],
                    horizon: r.horizon,
                    steps: r.steps,
                    paths: r.paths,
                    seed: r.seed,
                    terminal_only,
                };
                let expected = gbm_paths(&gbm).unwrap();
                let actual = merton_paths(&r).unwrap();
                assert_eq!(
                    actual.iter().map(|x| x.to_bits()).collect::<Vec<_>>(),
                    expected.iter().map(|x| x.to_bits()).collect::<Vec<_>>()
                );
            }
        }
    }
}

#[test]
fn terminal_layout_and_reproducibility_share_full_path_bits() {
    let r = request();
    let full = merton_paths(&r).unwrap();
    assert_eq!(full.len(), r.paths * (r.steps + 1));
    assert_eq!(full, merton_paths(&r).unwrap());
    assert_ne!(
        full,
        merton_paths(&MertonRequest { seed: 91, ..r }).unwrap()
    );
    let terminal = merton_paths(&MertonRequest {
        terminal_only: true,
        ..r
    })
    .unwrap();
    for (path, &value) in terminal.iter().enumerate() {
        assert_eq!(full[path * (r.steps + 1)], r.spot);
        assert_eq!(
            value.to_bits(),
            full[path * (r.steps + 1) + r.steps].to_bits()
        );
    }
}

#[test]
fn zero_horizon_returns_initial_spot_without_transition_arithmetic() {
    for terminal_only in [false, true] {
        let r = MertonRequest {
            horizon: 0.0,
            terminal_only,
            drift: Real::MAX,
            volatility: 1e150,
            ..request()
        };
        assert!(
            merton_paths(&r)
                .unwrap()
                .iter()
                .all(|x| x.to_bits() == r.spot.to_bits())
        );
    }
}

#[test]
fn all_zero_draw_coefficients_still_advance_three_streams() {
    let mut zero = Draws::new(42, 0.0).unwrap();
    let mut active = Draws::new(42, 2.0).unwrap();
    for _ in 0..20 {
        let (zd0, n0, zj0) = zero.next().unwrap();
        let (zd, _, zj) = active.next().unwrap();
        assert_eq!(n0, 0.0);
        assert_eq!(zd0, zd);
        assert_eq!(zj0, zj);
    }
    assert_eq!(zero.counts.next_u32(), active.counts.next_u32());
}

#[test]
fn derived_seed_wraps_are_nonzero_and_pin_stream_roles() {
    assert_eq!(shifted_seed(42, 0x9e37_79b9), 2654435811);
    assert_eq!(shifted_seed(42, 0xbb67_ae85), 3144134319);
    for seed in [1, 42, Natural::MAX] {
        assert!(shifted_seed(seed, 0x9e37_79b9) > 0);
        assert!(shifted_seed(seed, 0xbb67_ae85) > 0);
        assert_ne!(
            shifted_seed(seed, 0x9e37_79b9),
            shifted_seed(seed, 0xbb67_ae85)
        );
    }
    assert_eq!(shifted_seed(Natural::MAX, 0x9e37_79b9), 0x9e37_79b9);
    assert_eq!(shifted_seed(Natural::MAX, 0xbb67_ae85), 0xbb67_ae85);
}

#[test]
fn invalid_domains_and_dimension_overflows_are_errors() {
    for r in [
        MertonRequest {
            spot: 0.0,
            ..request()
        },
        MertonRequest {
            spot: Real::INFINITY,
            ..request()
        },
        MertonRequest {
            drift: Real::NAN,
            ..request()
        },
        MertonRequest {
            volatility: -0.1,
            ..request()
        },
        MertonRequest {
            jump_intensity: -1.0,
            ..request()
        },
        MertonRequest {
            log_mean_jump: Real::NAN,
            ..request()
        },
        MertonRequest {
            log_jump_volatility: -1.0,
            ..request()
        },
        MertonRequest {
            horizon: -1.0,
            ..request()
        },
        MertonRequest {
            steps: 0,
            ..request()
        },
        MertonRequest {
            paths: 0,
            ..request()
        },
        MertonRequest {
            seed: 0,
            ..request()
        },
        MertonRequest {
            steps: Size::MAX,
            ..request()
        },
        MertonRequest {
            paths: Size::MAX,
            ..request()
        },
        MertonRequest {
            steps: Size::MAX / 2,
            paths: 1,
            terminal_only: true,
            ..request()
        },
    ] {
        assert!(merton_paths(&r).is_err());
    }
    assert!(
        output_len(&MertonRequest {
            steps: Size::MAX,
            ..request()
        })
        .is_err()
    );
    assert!(
        output_len(&MertonRequest {
            paths: Size::MAX,
            ..request()
        })
        .is_err()
    );
    assert_eq!(
        output_len(&MertonRequest {
            terminal_only: true,
            steps: Size::MAX,
            ..request()
        })
        .unwrap(),
        request().paths
    );
}

#[test]
fn unrepresentable_parameters_and_inverse_count_domain_are_errors() {
    for r in [
        MertonRequest {
            volatility: Real::MAX,
            ..request()
        },
        MertonRequest {
            log_jump_volatility: Real::MAX,
            ..request()
        },
        MertonRequest {
            log_mean_jump: 1000.0,
            ..request()
        },
        MertonRequest {
            log_mean_jump: -1000.0,
            ..request()
        },
        MertonRequest {
            log_mean_jump: 700.0,
            jump_intensity: Real::MAX,
            ..request()
        },
        MertonRequest {
            jump_intensity: Real::MAX,
            horizon: Real::MAX,
            ..request()
        },
        MertonRequest {
            jump_intensity: 800.0,
            steps: 1,
            ..request()
        },
        MertonRequest {
            horizon: Real::from_bits(1),
            ..request()
        },
        MertonRequest {
            jump_intensity: Real::from_bits(1),
            horizon: 0.1,
            steps: 1,
            ..request()
        },
    ] {
        assert!(merton_paths(&r).is_err());
    }
}

#[test]
fn simulated_spot_overflow_underflow_and_mid_path_failure_return_errors() {
    let jumps = MertonRequest {
        volatility: 0.0,
        log_mean_jump: -700.0,
        log_jump_volatility: 0.0,
        paths: 1,
        ..request()
    };
    assert!(
        merton_paths(&MertonRequest {
            steps: 2,
            horizon: 0.5,
            ..jumps
        })
        .is_ok()
    );
    assert!(merton_paths(&jumps).is_err());
    assert!(
        merton_paths(&MertonRequest {
            terminal_only: true,
            ..jumps
        })
        .is_err()
    );
    for (drift, steps) in [(2000.0, 1), (-2000.0, 1), (1000.0, 4), (-1000.0, 4)] {
        let r = MertonRequest {
            drift,
            steps,
            jump_intensity: 0.0,
            volatility: 0.0,
            ..request()
        };
        assert!(merton_paths(&r).is_err());
        assert!(
            merton_paths(&MertonRequest {
                terminal_only: true,
                ..r
            })
            .is_err()
        );
    }
}

mod distribution;

fn assert_fixture(r: &MertonRequest, expected: &[Real]) {
    let full = merton_paths(r).unwrap();
    assert_eq!(full.len(), expected.len());
    for (&actual, &expected) in full.iter().zip(expected) {
        assert_close(actual, expected, 2e-14 * expected.abs().max(1.0));
    }
    let terminal = merton_paths(&MertonRequest {
        terminal_only: true,
        ..*r
    })
    .unwrap();
    for (path, &actual) in terminal.iter().enumerate() {
        let last = path * (r.steps + 1) + r.steps;
        assert_close(
            actual,
            expected[last],
            2e-14 * expected[last].abs().max(1.0),
        );
        assert_eq!(actual.to_bits(), full[last].to_bits());
    }
}

mod oracle;
mod oracle_additional;