libitofin 0.50.0

A ground-up Rust port of QuantLib: quantitative-finance primitives for pricing, risk, and numerical methods.
Documentation
use super::ReplicatingVarianceSwapEngine;
use super::test_market::{CALLS, Market, PUTS, close, curve, quote_handle, today};
use crate::handle::Handle;
use crate::instrument::Instrument;
use crate::instruments::VarianceSwap;
use crate::math::matrix::Matrix;
use crate::position::Position;
use crate::processes::GeneralizedBlackScholesProcess;
use crate::shared::{Shared, shared, shared_mut};
use crate::termstructures::volatility::{
    BlackConstantVol, BlackVarianceSurface, BlackVolTermStructure,
};
use crate::time::daycounters::actual360::Actual360;
use crate::time::daycounters::actual365fixed::Actual365Fixed;

#[test]
fn variance_replication_flat_strip_tends_to_volatility_squared_without_dividends() {
    let market = Market::new();
    market.rate.set_value(0.0);
    let calls: Vec<_> = (4000..=8000).map(|i| f64::from(i) * 0.025).collect();
    let puts: Vec<_> = (1..=4000).map(|i| f64::from(i) * 0.025).collect();
    let mut swap = market.swap(Position::Long, 0.04, &calls, &puts, 0.0125);
    close(swap.variance().unwrap(), 0.04, 1e-7);
    market.vol.set_value(0.0);
    close(swap.variance().unwrap(), 0.0, 1e-15);
}

#[test]
fn variance_replication_nonzero_dividend_limit_includes_boundary_term_and_signed_output() {
    let market = Market::new();
    market.dividend.set_value(0.03);
    let calls: Vec<_> = (4000..=8000).map(|i| f64::from(i) * 0.025).collect();
    let puts: Vec<_> = (1..=4000).map(|i| f64::from(i) * 0.025).collect();
    let mut swap = market.swap(Position::Long, 0.04, &calls, &puts, 0.0125);
    let time: f64 = 90.0 / 365.0;
    let expected =
        0.04 + 2.0 * 0.03 + 2.0 / time * (((0.05 - 0.03) * time).exp() - (0.05 * time).exp());
    close(swap.variance().unwrap(), expected, 1e-7);
    assert!((expected - 0.04).abs() > 1e-4);
    let calls: Vec<_> = (500..=4095).map(|i| f64::from(i) * 0.1).collect();
    let puts: Vec<_> = (1..=500).map(|i| f64::from(i) * 0.1).collect();
    market.dividend.set_value(0.2);
    let mut signed = market.swap(Position::Long, 0.04, &calls, &puts, 0.05);
    assert!(signed.variance().unwrap() < 0.0);
    assert!(signed.npv().unwrap() < 0.0);
}

#[test]
fn variance_replication_literature_smile_keeps_original_native_tolerance() {
    let market = Market::new();
    let strikes: Vec<_> = (50..=135).step_by(5).map(f64::from).collect();
    let mut vols = Matrix::with_size(strikes.len(), 1);
    for i in 0..strikes.len() {
        vols[(i, 0)] = 0.30 - 0.01 * i as f64;
    }
    let volatility = shared(
        BlackVarianceSurface::new(
            today(),
            None,
            &[today() + 90],
            strikes,
            &vols,
            Actual365Fixed::new(),
        )
        .unwrap(),
    ) as Shared<dyn BlackVolTermStructure>;
    let process = shared(GeneralizedBlackScholesProcess::new(
        quote_handle(&market.spot),
        Handle::new(curve(today(), &market.dividend, Actual365Fixed::new())),
        Handle::new(curve(today(), &market.rate, Actual365Fixed::new())),
        Handle::new(volatility),
    ));
    let mut swap = VarianceSwap::new(
        Position::Long,
        0.04,
        50000.0,
        today(),
        today() + 90,
        market.settings.clone(),
    )
    .unwrap();
    swap.base_mut().set_pricing_engine(shared_mut(
        ReplicatingVarianceSwapEngine::new(process, 5.0, CALLS, PUTS).unwrap(),
    ));
    close(swap.variance().unwrap(), 0.04189, 1e-4);
    assert_eq!(swap.option_weights().unwrap().len(), 19);
}

#[test]
fn variance_replication_rejects_forward_seasoned_and_mismatched_references() {
    let market = Market::new();
    for start in [today() - 1, today() + 1] {
        let mut swap = VarianceSwap::new(
            Position::Long,
            0.04,
            50000.0,
            start,
            today() + 90,
            market.settings.clone(),
        )
        .unwrap();
        swap.base_mut().set_pricing_engine(shared_mut(
            ReplicatingVarianceSwapEngine::new(market.process.clone(), 5.0, CALLS, PUTS).unwrap(),
        ));
        assert!(swap.npv().unwrap_err().message().contains("spot start"));
    }
    for side in 0..3 {
        let reference = |selected| {
            if side == selected {
                today() - 1
            } else {
                today()
            }
        };
        let process = shared(GeneralizedBlackScholesProcess::new(
            quote_handle(&market.spot),
            Handle::new(curve(reference(0), &market.dividend, Actual365Fixed::new())),
            Handle::new(curve(reference(1), &market.rate, Actual365Fixed::new())),
            Handle::new(shared(BlackConstantVol::new(
                reference(2),
                None,
                0.2,
                Actual365Fixed::new(),
            )) as Shared<dyn BlackVolTermStructure>),
        ));
        let mut swap = VarianceSwap::new(
            Position::Long,
            0.04,
            50000.0,
            today(),
            today() + 90,
            market.settings.clone(),
        )
        .unwrap();
        swap.base_mut().set_pricing_engine(shared_mut(
            ReplicatingVarianceSwapEngine::new(process, 5.0, CALLS, PUTS).unwrap(),
        ));
        assert!(
            swap.npv()
                .unwrap_err()
                .message()
                .contains("reference dates")
        );
    }
}

#[test]
fn variance_replication_accepts_distinct_daycounters_and_existing_negative_raw_vol() {
    let market = Market::new();
    let process = shared(GeneralizedBlackScholesProcess::new(
        quote_handle(&market.spot),
        Handle::new(curve(today(), &market.dividend, Actual360::new())),
        Handle::new(curve(today(), &market.rate, Actual365Fixed::new())),
        Handle::new(
            shared(BlackConstantVol::new(today(), None, 0.2, Actual360::new()))
                as Shared<dyn BlackVolTermStructure>,
        ),
    ));
    let mut swap = VarianceSwap::new(
        Position::Long,
        0.04,
        50000.0,
        today(),
        today() + 90,
        market.settings.clone(),
    )
    .unwrap();
    swap.base_mut().set_pricing_engine(shared_mut(
        ReplicatingVarianceSwapEngine::new(process, 5.0, CALLS, PUTS).unwrap(),
    ));
    assert!(swap.variance().unwrap().is_finite());
    let mut swap = market.swap(Position::Long, 0.04, CALLS, PUTS, 5.0);
    let original = swap.variance().unwrap();
    market.vol.set_value(-0.2);
    close(swap.variance().unwrap(), original, 0.0);
}