libitofin 0.40.0

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

fn calculator(option: OptionType, delta_type: DeltaType) -> BlackDeltaCalculator {
    BlackDeltaCalculator::new(option, delta_type, 1.421, 0.997306, 0.992266, 0.1180654).unwrap()
}

#[test]
fn quantlib_published_delta_and_strike_cases() {
    let rows = [
        (
            OptionType::Call,
            DeltaType::Spot,
            1.421,
            0.997306,
            0.992266,
            0.1180654,
            1.608080,
            0.15,
        ),
        (
            OptionType::Call,
            DeltaType::PaSpot,
            1.421,
            0.997306,
            0.992266,
            0.1180654,
            1.600545,
            0.15,
        ),
        (
            OptionType::Call,
            DeltaType::Fwd,
            1.421,
            0.997306,
            0.992266,
            0.1180654,
            1.609029,
            0.15,
        ),
        (
            OptionType::Call,
            DeltaType::PaFwd,
            1.421,
            0.997306,
            0.992266,
            0.1180654,
            1.601550,
            0.15,
        ),
        (
            OptionType::Call,
            DeltaType::Spot,
            122.121,
            0.9695434,
            0.9872347,
            0.0887676,
            119.8031,
            0.67,
        ),
        (
            OptionType::Call,
            DeltaType::PaSpot,
            122.121,
            0.9695434,
            0.9872347,
            0.0887676,
            117.7096,
            0.67,
        ),
        (
            OptionType::Call,
            DeltaType::Fwd,
            122.121,
            0.9695434,
            0.9872347,
            0.0887676,
            120.0592,
            0.67,
        ),
        (
            OptionType::Call,
            DeltaType::PaFwd,
            122.121,
            0.9695434,
            0.9872347,
            0.0887676,
            118.0532,
            0.67,
        ),
        (
            OptionType::Put,
            DeltaType::Spot,
            3.4582,
            0.99979,
            0.9250616,
            0.3199034,
            4.964924,
            -0.821,
        ),
        (
            OptionType::Put,
            DeltaType::PaSpot,
            3.4582,
            0.99979,
            0.9250616,
            0.3199034,
            3.778327,
            -0.821,
        ),
        (
            OptionType::Put,
            DeltaType::Fwd,
            3.4582,
            0.99979,
            0.9250616,
            0.3199034,
            4.51896,
            -0.821,
        ),
        (
            OptionType::Put,
            DeltaType::PaFwd,
            3.4582,
            0.99979,
            0.9250616,
            0.3199034,
            3.65728,
            -0.821,
        ),
        (
            OptionType::Put,
            DeltaType::Spot,
            103.0,
            0.99482,
            0.98508,
            0.07247845,
            97.47,
            -0.25,
        ),
        (
            OptionType::Put,
            DeltaType::PaSpot,
            103.0,
            0.99482,
            0.98508,
            0.07247845,
            97.22,
            -0.25,
        ),
    ];
    for (option, convention, spot, domestic, foreign, deviation, strike, delta) in rows {
        let calc =
            BlackDeltaCalculator::new(option, convention, spot, domestic, foreign, deviation)
                .unwrap();
        assert!((calc.delta_from_strike(strike).unwrap() - delta).abs() < 1e-3);
        assert!((calc.strike_from_delta(delta).unwrap() - strike).abs() < 1e-2);
    }
}

#[test]
fn premium_adjusted_call_selects_high_strike_root() {
    for convention in [DeltaType::PaSpot, DeltaType::PaFwd] {
        let calc = calculator(OptionType::Call, convention);
        let low_strike = 0.5;
        let delta = calc.delta_from_strike(low_strike).unwrap();
        let high_strike = calc.strike_from_delta(delta).unwrap();
        assert!(high_strike > 1.4);
        assert!((calc.delta_from_strike(high_strike).unwrap() - delta).abs() < 1e-10);
        assert!(calc.strike_from_delta(0.9).is_err());
    }
}

#[test]
fn atm_convention_constraints_and_mutators_match_quantlib() {
    let mut calc = calculator(OptionType::Call, DeltaType::Spot);
    assert!(calc.atm_strike(AtmType::PutCall50).is_err());
    assert!(calc.atm_strike(AtmType::Null).is_err());
    let ordinary = calc.atm_strike(AtmType::DeltaNeutral).unwrap();
    calc.set_delta_type(DeltaType::PaSpot);
    assert!(calc.atm_strike(AtmType::PutCall50).is_err());
    assert!(calc.atm_strike(AtmType::DeltaNeutral).unwrap() < ordinary);
    calc.set_delta_type(DeltaType::PaFwd);
    assert!(calc.atm_strike(AtmType::PutCall50).is_err());
    calc.set_delta_type(DeltaType::Fwd);
    assert_eq!(calc.atm_strike(AtmType::PutCall50).unwrap(), ordinary);
    assert_eq!(calc.atm_strike(AtmType::GammaMax).unwrap(), ordinary);
    assert_eq!(calc.atm_strike(AtmType::VegaMax).unwrap(), ordinary);
    calc.set_option_type(OptionType::Put);
    assert!(calc.delta_from_strike(1.6).unwrap() < 0.0);
}

#[test]
fn zero_volatility_and_zero_strike_have_defined_deltas() {
    for convention in [
        DeltaType::Spot,
        DeltaType::Fwd,
        DeltaType::PaSpot,
        DeltaType::PaFwd,
    ] {
        for option in [OptionType::Call, OptionType::Put] {
            let calc = BlackDeltaCalculator::new(option, convention, 1.0, 1.0, 1.0, 0.0).unwrap();
            let sign = option as i32 as Real;
            assert_eq!(calc.delta_from_strike(1.0).unwrap(), sign * 0.5);
            assert!(calc.strike_from_delta(sign * 0.25).is_err());
            let zero_delta = calc.delta_from_strike(0.0).unwrap();
            assert_eq!(
                zero_delta,
                if option == OptionType::Call
                    && matches!(convention, DeltaType::Spot | DeltaType::Fwd)
                {
                    1.0
                } else {
                    0.0
                }
            );
            let large_delta = calc.delta_from_strike(2.0).unwrap();
            assert_eq!(
                large_delta,
                if option == OptionType::Call {
                    0.0
                } else if matches!(convention, DeltaType::Spot | DeltaType::Fwd) {
                    -1.0
                } else {
                    -2.0
                }
            );
        }
    }
}

#[test]
fn invalid_inputs_and_inversion_boundaries_error() {
    for bad in [Real::NAN, Real::INFINITY, -1.0] {
        assert!(
            BlackDeltaCalculator::new(OptionType::Call, DeltaType::Fwd, bad, 1.0, 1.0, 0.1)
                .is_err()
        );
        assert!(
            BlackDeltaCalculator::new(OptionType::Call, DeltaType::Fwd, 1.0, bad, 1.0, 0.1)
                .is_err()
        );
        assert!(
            BlackDeltaCalculator::new(OptionType::Call, DeltaType::Fwd, 1.0, 1.0, bad, 0.1)
                .is_err()
        );
        assert!(
            BlackDeltaCalculator::new(OptionType::Call, DeltaType::Fwd, 1.0, 1.0, 1.0, bad)
                .is_err()
        );
        assert!(
            calculator(OptionType::Call, DeltaType::Fwd)
                .delta_from_strike(bad)
                .is_err()
        );
    }
    let calc = calculator(OptionType::Call, DeltaType::Fwd);
    for bad in [Real::NAN, Real::INFINITY, -0.25, 0.0, 1.0, 1.1] {
        assert!(calc.strike_from_delta(bad).is_err());
    }
}

#[test]
fn compiled_quantlib_1_43_delta_and_strike_oracle() {
    let rows = [
        (
            OptionType::Call,
            DeltaType::Spot,
            0.16013974647836293,
            1.6080794396498383,
        ),
        (
            OptionType::Call,
            DeltaType::Fwd,
            0.16138792065672203,
            1.6090292778757842,
        ),
        (
            OptionType::Call,
            DeltaType::PaSpot,
            0.15067826139118903,
            1.6005703561512619,
        ),
        (
            OptionType::Call,
            DeltaType::PaFwd,
            0.15185269009639454,
            1.6015501875673384,
        ),
        (
            OptionType::Put,
            DeltaType::Spot,
            -0.832126253521637,
            1.260473835686862,
        ),
        (
            OptionType::Put,
            DeltaType::Fwd,
            -0.838612079343278,
            1.2597297558566178,
        ),
        (
            OptionType::Put,
            DeltaType::PaSpot,
            -0.9722560102485015,
            1.2546219991778351,
        ),
        (
            OptionType::Put,
            DeltaType::PaFwd,
            -0.9798340467661911,
            1.2539067336573,
        ),
    ];
    for (option, convention, expected_delta, expected_strike) in rows {
        let calc = calculator(option, convention);
        let delta = calc.delta_from_strike(1.6).unwrap();
        let strike = calc
            .strike_from_delta(option as i32 as Real * 0.15)
            .unwrap();
        assert!(
            (delta - expected_delta).abs() < 1e-14,
            "{option:?} {convention:?}: {delta}"
        );
        assert!(
            (strike - expected_strike).abs() < 1e-10,
            "{option:?} {convention:?}: {strike}"
        );
        assert!((calc.atm_strike(AtmType::Spot).unwrap() - 1.421).abs() < 1e-14);
        assert!((calc.atm_strike(AtmType::Fwd).unwrap() - 1.413818813884605).abs() < 1e-14);
        let neutral = if matches!(convention, DeltaType::Spot | DeltaType::Fwd) {
            1.4237071536606858
        } else {
            1.4039991534456169
        };
        assert!((calc.atm_strike(AtmType::DeltaNeutral).unwrap() - neutral).abs() < 1e-14);
    }
}