libitofin 0.52.0

A ground-up Rust port of QuantLib: quantitative-finance primitives for pricing, risk, and numerical methods.
Documentation
use super::*;
use crate::handle::Handle;
use crate::instrument::Instrument;
use crate::instruments::VarianceSwap;
use crate::pricingengines::forward::test_market::{Market, today};
use crate::shared::shared_mut;
use crate::termstructures::volatility::{LocalVolTermStructure, VolatilityTermStructure};
use crate::termstructures::{TermStructure, TermStructureBase};
use crate::time::businessdayconvention::BusinessDayConvention;
use crate::time::date::Date;
use crate::time::daycounters::actual365fixed::Actual365Fixed;

struct StateVol {
    base: TermStructureBase,
    invalid: bool,
}
impl AsObservable for StateVol {
    fn observable(&self) -> &Observable {
        self.base.observable()
    }
}
impl TermStructure for StateVol {
    fn base(&self) -> &TermStructureBase {
        &self.base
    }
    fn max_date(&self) -> Date {
        Date::max_date()
    }
}
impl VolatilityTermStructure for StateVol {
    fn business_day_convention(&self) -> BusinessDayConvention {
        BusinessDayConvention::Following
    }
    fn min_strike(&self) -> f64 {
        f64::MIN
    }
    fn max_strike(&self) -> f64 {
        f64::MAX
    }
}
impl LocalVolTermStructure for StateVol {
    fn local_vol_impl(&self, _time: f64, state: f64) -> QlResult<f64> {
        Ok(if self.invalid {
            -0.1
        } else {
            0.15 + 0.1 * state / (state + 100.0)
        })
    }
}

pub(super) fn process(market: &Market, invalid: bool) -> Shared<GeneralizedBlackScholesProcess> {
    let local = shared(StateVol {
        base: TermStructureBase::with_reference_date(today(), None, Some(Actual365Fixed::new())),
        invalid,
    });
    shared(GeneralizedBlackScholesProcess::with_local_vol(
        market.process.state_variable(),
        market.process.dividend_yield(),
        market.process.risk_free_rate(),
        market.process.black_volatility(),
        Handle::new(local as Shared<dyn LocalVolTermStructure>),
    ))
}

fn swap(
    market: &Market,
    process: Shared<GeneralizedBlackScholesProcess>,
    position: Position,
    samples: Option<usize>,
    tolerance: Option<f64>,
    maximum: Option<usize>,
    seed: u64,
) -> VarianceSwap {
    let mut swap = VarianceSwap::new(
        position,
        0.04,
        50_000.0,
        today(),
        today() + 90,
        market.settings.clone(),
    )
    .unwrap();
    swap.base_mut().set_pricing_engine(shared_mut(
        MCVarianceSwapEngine::new(process, Some(7), None, samples, tolerance, maximum, seed)
            .unwrap(),
    ));
    swap
}

#[test]
fn state_dependent_local_vol_has_seeded_error_and_signed_cash_error() {
    let market = Market::new();
    let process = process(&market, false);
    let mut long = swap(
        &market,
        process.clone(),
        Position::Long,
        Some(1023),
        None,
        None,
        42,
    );
    let mut short = swap(
        &market,
        process.clone(),
        Position::Short,
        Some(1023),
        None,
        None,
        42,
    );
    let mut other = swap(&market, process, Position::Long, Some(1023), None, None, 43);
    let variance = long.variance().unwrap();
    assert!(long.variance_error().unwrap() > 0.0);
    assert!(long.error_estimate().unwrap() > 0.0);
    assert_eq!(variance, short.variance().unwrap());
    assert_eq!(
        long.error_estimate().unwrap(),
        -short.error_estimate().unwrap()
    );
    assert_eq!(
        long.variance_error().unwrap(),
        short.variance_error().unwrap()
    );
    assert_ne!(variance, other.variance().unwrap());
    long.recalculate().unwrap();
    assert_eq!(variance, long.variance().unwrap());
    market.spot.set_value(110.0);
    assert_ne!(variance, long.variance().unwrap());
    market.rate.set_value(0.08);
    assert!(long.npv().unwrap().is_finite());
    market.dividend.set_value(0.03);
    assert!(long.npv().unwrap().is_finite());
}

#[test]
fn state_dependent_tolerance_batches_and_exhaustion() {
    let market = Market::new();
    let process = process(&market, false);
    let mut result = swap(
        &market,
        process.clone(),
        Position::Long,
        None,
        Some(1e-5),
        Some(16368),
        42,
    );
    assert!(result.variance().unwrap().is_finite());
    assert!(result.variance_error().unwrap() <= 1e-5);
    assert!(result.samples().unwrap() > 1023);
    let mut exhausted = swap(
        &market,
        process,
        Position::Long,
        None,
        Some(1e-15),
        Some(1023),
        42,
    );
    assert!(exhausted.variance().is_err());
    assert!(exhausted.samples().is_err());
    assert!(exhausted.variance_error().is_err());
}

#[test]
fn negative_external_diffusion_is_not_squared_into_a_valid_result() {
    let market = Market::new();
    let mut result = swap(
        &market,
        process(&market, true),
        Position::Long,
        Some(1023),
        None,
        None,
        42,
    );
    let error = result.variance().unwrap_err();
    assert!(error.message().contains("diffusion"));
    assert!(result.samples().is_err());
}