libitofin 0.42.0

A ground-up Rust port of QuantLib: quantitative-finance primitives for pricing, risk, and numerical methods.
Documentation
use super::tests::parameters;
use super::*;
use crate::exercise::EuropeanExercise;
use crate::handle::{Handle, RelinkableHandle};
use crate::instrument::Instrument;
use crate::instruments::{PlainVanillaPayoff, VanillaOption};
use crate::interestrate::Compounding;
use crate::math::optimization::endcriteria::{EndCriteria, EndCriteriaType};
use crate::math::optimization::method::OptimizationMethod;
use crate::math::optimization::problem::Problem;
use crate::math::optimization::simplex::Simplex;
use crate::models::calibrate;
use crate::models::calibrationhelper::CalibrationHelper;
use crate::option::OptionType;
use crate::pricingengines::vanilla::AnalyticGjrGarchEngine;
use crate::quotes::{Quote, SimpleQuote};
use crate::settings::Settings;
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::test_support::{Flag, as_observer};
use crate::time::{
    date::{Date, Month},
    daycounters::actual360::Actual360,
    frequency::Frequency,
};

#[derive(Clone)]
enum Market {
    Spot(Shared<SimpleQuote>),
    Curve(RelinkableHandle<dyn YieldTermStructure>),
}

fn reference() -> Date {
    Date::new(15, Month::June, 2026)
}

fn curve(date: Date) -> Shared<dyn YieldTermStructure> {
    shared(FlatForward::with_rate(
        date,
        0.03,
        Actual360::new(),
        Compounding::Continuous,
        Frequency::Annual,
    ))
}

impl Market {
    fn invalidate(&self) {
        match self {
            Self::Spot(spot) => {
                spot.reset();
            }
            Self::Curve(curve_handle) => curve_handle.link_to(curve(Date::null())),
        }
    }

    fn repair(&self) {
        match self {
            Self::Spot(spot) => {
                spot.set_value(100.0);
            }
            Self::Curve(curve_handle) => curve_handle.link_to(curve(reference())),
        }
    }

    fn assert_invalid(&self, model: &SharedMut<GjrGarchModel>) {
        match self {
            Self::Spot(spot) => {
                assert!(spot.value().is_err());
                assert!(model.borrow().process().initial_values().is_err());
            }
            Self::Curve(curve_handle) => {
                assert!(
                    curve_handle
                        .handle()
                        .current_link()
                        .unwrap()
                        .reference_date()
                        .is_err()
                );
                assert!(model.borrow().process().time(&(reference() + 90)).is_err());
            }
        }
    }
}

fn fixture(invalidate_curve: bool) -> (SharedMut<GjrGarchModel>, Market) {
    let spot = shared(SimpleQuote::new(100.0));
    let risk_free = RelinkableHandle::new(curve(reference()));
    let process = shared(
        GjrGarchProcess::new(
            risk_free.handle(),
            Handle::new(curve(reference())),
            Handle::new(Shared::clone(&spot) as Shared<dyn Quote>),
            parameters(),
            GjrGarchDiscretization::FullTruncation,
        )
        .unwrap(),
    );
    let market = if invalidate_curve {
        Market::Curve(risk_free)
    } else {
        Market::Spot(spot)
    };
    (GjrGarchModel::new(process).unwrap(), market)
}

fn price(model: &SharedMut<GjrGarchModel>) -> QlResult<Real> {
    let settings = shared(Settings::new());
    settings.set_evaluation_date(reference());
    let mut option = VanillaOption::new(
        shared(PlainVanillaPayoff::new(OptionType::Call, 100.0)),
        shared(EuropeanExercise::new(reference() + 90)),
        settings,
    );
    option
        .base_mut()
        .set_pricing_engine(shared_mut(AnalyticGjrGarchEngine::new(SharedMut::clone(
            model,
        ))));
    option.npv()
}

struct VarianceHelper {
    model: SharedMut<GjrGarchModel>,
    invalidate_after_first_call: Option<Market>,
    calls: usize,
}

impl CalibrationHelper for VarianceHelper {
    fn calibration_error(&mut self) -> QlResult<Real> {
        self.calls += 1;
        if self.calls > 1
            && let Some(market) = &self.invalidate_after_first_call
        {
            market.invalidate();
            crate::fail!("deliberate final helper failure after market invalidation");
        }
        Ok(self.model.borrow().process().initial_values()?[1] - 0.12)
    }
}

fn helper(
    model: &SharedMut<GjrGarchModel>,
    market: Option<Market>,
) -> SharedMut<dyn CalibrationHelper> {
    shared_mut(VarianceHelper {
        model: SharedMut::clone(model),
        invalidate_after_first_call: market,
        calls: 0,
    })
}

fn criteria() -> EndCriteria {
    EndCriteria::new(1000, Some(50), 1e-12, 1e-12, None).unwrap()
}

struct FailureAfterTrial {
    model: SharedMut<GjrGarchModel>,
    invalidate_before_optimizer_error: Option<Market>,
}

impl OptimizationMethod for FailureAfterTrial {
    fn minimize(
        &mut self,
        problem: &mut Problem<'_>,
        _: &EndCriteria,
    ) -> QlResult<EndCriteriaType> {
        let trial = Array::from([3e-6, 0.06, 0.81, 0.12, 0.2, 0.0002]);
        let values = problem.values(&trial);
        assert!(values.iter().all(|value| value.is_finite()));
        assert_eq!(self.model.borrow().calibrated_model().params(), trial);
        problem.set_current_value(trial);
        if let Some(market) = &self.invalidate_before_optimizer_error {
            market.invalidate();
            crate::fail!("deliberate optimizer failure after market invalidation");
        }
        Ok(EndCriteriaType::StationaryPoint)
    }
}

fn assert_market_failure_restores_snapshot(invalidate_curve: bool, fail_optimizer: bool) {
    let (model, market) = fixture(invalidate_curve);
    calibrate(
        &model,
        &[helper(&model, None)],
        &mut Simplex::new(0.0001),
        &criteria(),
        None,
        Vec::new(),
        vec![true, true, true, true, true, false],
    )
    .unwrap();
    let (params, process, end_criteria, residuals, evaluations) = {
        let borrowed = model.borrow();
        let calibrated = borrowed.calibrated_model();
        assert!(calibrated.end_criteria().succeeded());
        assert!(!calibrated.problem_values().is_empty());
        assert!(calibrated.function_evaluation() > 0);
        (
            calibrated.params(),
            borrowed.process(),
            calibrated.end_criteria(),
            calibrated.problem_values().clone(),
            calibrated.function_evaluation(),
        )
    };
    let initial_price = price(&model).unwrap();
    let flag = Flag::new();
    model
        .borrow()
        .observable()
        .register_observer(&as_observer(&flag));
    let mut method = FailureAfterTrial {
        model: SharedMut::clone(&model),
        invalidate_before_optimizer_error: fail_optimizer.then(|| market.clone()),
    };
    let helper = helper(&model, (!fail_optimizer).then(|| market.clone()));
    let error = calibrate(
        &model,
        &[helper],
        &mut method,
        &criteria(),
        None,
        Vec::new(),
        Vec::new(),
    )
    .unwrap_err();
    let expected = if fail_optimizer {
        "deliberate optimizer failure after market invalidation"
    } else {
        "nonfinite final calibration residuals"
    };
    assert_eq!(error.message(), expected);
    {
        let borrowed = model.borrow();
        let calibrated = borrowed.calibrated_model();
        assert_eq!(calibrated.params(), params);
        assert!(Shared::ptr_eq(&borrowed.process(), &process));
        assert_eq!(calibrated.end_criteria(), end_criteria);
        assert_eq!(calibrated.problem_values(), &residuals);
        assert_eq!(calibrated.function_evaluation(), evaluations);
    }
    assert!(Flag::is_up(&flag));
    market.assert_invalid(&model);
    assert!(price(&model).is_err());
    market.repair();
    assert_eq!(model.borrow().calibrated_model().params(), params);
    let repaired_price = price(&model).unwrap();
    assert!((repaired_price - initial_price).abs() < 1e-12);
}

#[test]
fn optimizer_failure_restores_model_snapshot_without_restoring_invalid_spot() {
    assert_market_failure_restores_snapshot(false, true);
}

#[test]
fn final_helper_failure_restores_model_snapshot_without_restoring_invalid_spot() {
    assert_market_failure_restores_snapshot(false, false);
}

#[test]
fn optimizer_failure_restores_model_snapshot_without_restoring_invalid_curve() {
    assert_market_failure_restores_snapshot(true, true);
}

#[test]
fn final_helper_failure_restores_model_snapshot_without_restoring_invalid_curve() {
    assert_market_failure_restores_snapshot(true, false);
}