use crate::errors::QlResult;
use crate::handle::Handle;
use crate::math::array::Array;
use crate::math::optimization::constraint::{CompositeConstraint, Constraint};
use crate::math::optimization::costfunction::CostFunction;
use crate::math::optimization::endcriteria::{EndCriteria, EndCriteriaType};
use crate::math::optimization::method::OptimizationMethod;
use crate::math::optimization::problem::Problem;
use crate::math::optimization::projectedconstraint::ProjectedConstraint;
use crate::math::optimization::projection::Projection;
use crate::models::calibrationhelper::CalibrationHelper;
use crate::models::parameter::Parameter;
use crate::patterns::observable::{AsObservable, Observable, Observer, ResetThenNotify};
use crate::require;
use crate::shared::{Shared, SharedMut, shared};
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::types::{Integer, Real, Size};
pub struct CalibratedModel {
arguments: Vec<Parameter>,
observable: Shared<Observable>,
updater: SharedMut<ResetThenNotify>,
end_criteria: EndCriteriaType,
problem_values: Array,
function_evaluation: Integer,
}
impl CalibratedModel {
pub fn new(n_arguments: Size) -> CalibratedModel {
let observable = shared(Observable::new());
let updater = ResetThenNotify::forwarding(Shared::clone(&observable));
CalibratedModel {
arguments: vec![Parameter::default(); n_arguments],
observable,
updater,
end_criteria: EndCriteriaType::None,
problem_values: Array::new(),
function_evaluation: 0,
}
}
pub fn arguments(&self) -> &[Parameter] {
&self.arguments
}
pub fn arguments_mut(&mut self) -> &mut [Parameter] {
&mut self.arguments
}
pub fn params(&self) -> Array {
let size = self.arguments.iter().map(Parameter::size).sum();
let mut params = Array::with_size(size);
let mut k = 0;
for argument in &self.arguments {
for j in 0..argument.size() {
params[k] = argument.params()[j];
k += 1;
}
}
params
}
pub fn write_params(&mut self, params: &Array) -> QlResult<()> {
let mut p = 0;
for argument in &mut self.arguments {
for j in 0..argument.size() {
require!(p < params.size(), "parameter array too small");
argument.set_param(j, params[p]);
p += 1;
}
}
require!(p == params.size(), "parameter array too big!");
Ok(())
}
pub fn set_params(&mut self, params: &Array) -> QlResult<()> {
self.write_params(params)?;
self.observable.notify_observers();
Ok(())
}
pub fn constraint(&self) -> PrivateConstraint {
PrivateConstraint {
arguments: self.arguments.clone(),
}
}
pub fn end_criteria(&self) -> EndCriteriaType {
self.end_criteria
}
pub fn problem_values(&self) -> &Array {
&self.problem_values
}
pub fn function_evaluation(&self) -> Integer {
self.function_evaluation
}
pub fn as_observer(&self) -> SharedMut<dyn Observer> {
self.updater.clone() as SharedMut<dyn Observer>
}
fn shared_observable(&self) -> Shared<Observable> {
self.observable.clone()
}
}
impl AsObservable for CalibratedModel {
fn observable(&self) -> &Observable {
&self.observable
}
}
pub trait CalibratedModelHolder {
fn calibrated_model(&self) -> &CalibratedModel;
fn calibrated_model_mut(&mut self) -> &mut CalibratedModel;
fn generate_arguments(&mut self) {}
fn set_params(&mut self, params: &Array) -> QlResult<()> {
self.calibrated_model_mut().write_params(params)?;
self.generate_arguments();
self.calibrated_model().observable().notify_observers();
Ok(())
}
}
pub struct TermStructureConsistentModel {
term_structure: Handle<dyn YieldTermStructure>,
}
impl TermStructureConsistentModel {
pub fn new(term_structure: Handle<dyn YieldTermStructure>) -> TermStructureConsistentModel {
TermStructureConsistentModel { term_structure }
}
pub fn term_structure(&self) -> &Handle<dyn YieldTermStructure> {
&self.term_structure
}
}
pub fn register_with_term_structure<M: CalibratedModelHolder + 'static>(
model: &SharedMut<M>,
handle: &Handle<dyn YieldTermStructure>,
) -> SharedMut<dyn Observer> {
let observable = model.borrow().calibrated_model().shared_observable();
let weak = SharedMut::downgrade(model);
let observer = ResetThenNotify::broadcasting(observable, move || {
if let Some(model) = weak.upgrade() {
model.borrow_mut().generate_arguments();
}
}) as SharedMut<dyn Observer>;
handle.register_observer(&observer);
observer
}
struct CalibrationFunction<M: CalibratedModelHolder> {
model: SharedMut<M>,
instruments: Vec<SharedMut<dyn CalibrationHelper>>,
weights: Vec<Real>,
projection: Projection,
}
impl<M: CalibratedModelHolder> CalibrationFunction<M> {
fn set_params(&self, full: &Array) -> bool {
self.model.borrow_mut().set_params(full).is_ok()
}
}
impl<M: CalibratedModelHolder> CostFunction for CalibrationFunction<M> {
fn values(&self, params: &Array) -> Array {
let full = self.projection.include(params);
let n = self.instruments.len();
if !self.set_params(&full) {
return Array::filled(n, Real::NAN);
}
let mut values = Array::with_size(n);
for (i, instrument) in self.instruments.iter().enumerate() {
values[i] = match instrument.borrow_mut().calibration_error() {
Ok(error) => error * self.weights[i].sqrt(),
Err(_) => Real::NAN,
};
}
values
}
fn value(&self, params: &Array) -> Real {
let full = self.projection.include(params);
if !self.set_params(&full) {
return Real::NAN;
}
let mut value = 0.0;
for (i, instrument) in self.instruments.iter().enumerate() {
let diff = match instrument.borrow_mut().calibration_error() {
Ok(error) => error,
Err(_) => return Real::NAN,
};
value += diff * diff * self.weights[i];
}
value.sqrt()
}
fn finite_difference_epsilon(&self) -> Real {
1e-6
}
}
pub fn calibrate<M: CalibratedModelHolder>(
model: &SharedMut<M>,
instruments: &[SharedMut<dyn CalibrationHelper>],
method: &mut dyn OptimizationMethod,
end_criteria: &EndCriteria,
additional_constraint: Option<Box<dyn Constraint>>,
weights: Vec<Real>,
fix_parameters: Vec<bool>,
) -> QlResult<()> {
require!(!instruments.is_empty(), "no instruments provided");
let private = model.borrow().calibrated_model().constraint();
let constraint: Box<dyn Constraint> = match additional_constraint {
None => Box::new(private),
Some(additional) => Box::new(CompositeConstraint::new(private, additional)),
};
require!(
weights.is_empty() || weights.len() == instruments.len(),
"mismatch between number of instruments ({}) and weights ({})",
instruments.len(),
weights.len()
);
let weights = if weights.is_empty() {
vec![1.0; instruments.len()]
} else {
weights
};
let prms = model.borrow().calibrated_model().params();
require!(
fix_parameters.is_empty() || fix_parameters.len() == prms.size(),
"mismatch between number of parameters ({}) and fixed-parameter specs ({})",
prms.size(),
fix_parameters.len()
);
let projection = Projection::new(&prms, fix_parameters)?;
let projected_constraint = ProjectedConstraint::new(constraint, projection.clone());
let function = CalibrationFunction {
model: SharedMut::clone(model),
instruments: instruments.to_vec(),
weights,
projection: projection.clone(),
};
let mut problem = Problem::new(&function, &projected_constraint, projection.project(&prms));
let end_criteria_result = method.minimize(&mut problem, end_criteria)?;
let result = problem.current_value().clone();
model
.borrow_mut()
.set_params(&projection.include(&result))?;
let problem_values = problem.values(&result);
let function_evaluation = problem.function_evaluation();
{
let mut borrowed = model.borrow_mut();
let calibrated = borrowed.calibrated_model_mut();
calibrated.end_criteria = end_criteria_result;
calibrated.problem_values = problem_values;
calibrated.function_evaluation = function_evaluation;
}
model
.borrow()
.calibrated_model()
.observable()
.notify_observers();
Ok(())
}
pub fn calibration_value<M: CalibratedModelHolder>(
model: &SharedMut<M>,
params: &Array,
instruments: &[SharedMut<dyn CalibrationHelper>],
) -> QlResult<Real> {
let weights = vec![1.0; instruments.len()];
let projection = Projection::new(params, Vec::new())?;
let function = CalibrationFunction {
model: SharedMut::clone(model),
instruments: instruments.to_vec(),
weights,
projection,
};
Ok(function.value(params))
}
pub struct PrivateConstraint {
arguments: Vec<Parameter>,
}
impl PrivateConstraint {
fn bound(&self, params: &Array, f: impl Fn(&dyn Constraint, &Array) -> Array) -> Array {
let total: Size = self.arguments.iter().map(Parameter::size).sum();
let mut result = Array::with_size(total);
let mut k = 0;
let mut k2 = 0;
for argument in &self.arguments {
let size = argument.size();
let mut partial = Array::with_size(size);
for j in 0..size {
partial[j] = params[k];
k += 1;
}
let tmp = f(argument.constraint(), &partial);
for j in 0..size {
result[k2] = tmp[j];
k2 += 1;
}
}
result
}
}
impl Constraint for PrivateConstraint {
fn test(&self, params: &Array) -> bool {
let mut k = 0;
for argument in &self.arguments {
let size = argument.size();
let mut test_params = Array::with_size(size);
for j in 0..size {
test_params[j] = params[k];
k += 1;
}
if !argument.test_params(&test_params) {
return false;
}
}
true
}
fn upper_bound(&self, params: &Array) -> Array {
self.bound(params, |constraint, partial| {
constraint.upper_bound(partial)
})
}
fn lower_bound(&self, params: &Array) -> Array {
self.bound(params, |constraint, partial| {
constraint.lower_bound(partial)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::RelinkableHandle;
use crate::interestrate::Compounding;
use crate::math::optimization::constraint::{NoConstraint, PositiveConstraint};
use crate::math::optimization::levenbergmarquardt::LevenbergMarquardt;
use crate::models::calibrationhelper::CalibrationHelper;
use crate::models::parameter::ConstantParameter;
use crate::patterns::observable::Observer;
use crate::shared::{Shared, SharedMut, WeakMut, shared, shared_mut};
use crate::termstructures::yields::FlatForward;
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::types::Real;
use std::cell::Cell;
use std::rc::Rc;
struct UpdateCounter {
count: usize,
}
impl Observer for UpdateCounter {
fn update(&mut self) {
self.count += 1;
}
}
fn two_argument_model() -> CalibratedModel {
let mut model = CalibratedModel::new(2);
model.arguments_mut()[0] =
ConstantParameter::new(0.1, Rc::new(PositiveConstraint)).unwrap();
model.arguments_mut()[1] = ConstantParameter::new(0.2, Rc::new(NoConstraint)).unwrap();
model
}
#[test]
fn params_flattens_arguments_in_order() {
let model = two_argument_model();
assert_eq!(model.params(), Array::from([0.1, 0.2]));
}
#[test]
fn set_params_round_trips_through_params() {
let mut model = two_argument_model();
model.set_params(&Array::from([0.3, 0.4])).unwrap();
assert_eq!(model.params(), Array::from([0.3, 0.4]));
assert_eq!(model.arguments()[0].value(0.0), 0.3);
assert_eq!(model.arguments()[1].value(0.0), 0.4);
}
#[test]
fn set_params_rejects_too_few_values() {
let mut model = two_argument_model();
let err = model.set_params(&Array::from([0.3])).unwrap_err();
assert_eq!(err.message(), "parameter array too small");
}
#[test]
fn set_params_rejects_too_many_values() {
let mut model = two_argument_model();
let err = model.set_params(&Array::from([0.3, 0.4, 0.5])).unwrap_err();
assert_eq!(err.message(), "parameter array too big!");
}
#[test]
fn private_constraint_tests_each_argument_on_its_own_slice() {
let constraint = two_argument_model().constraint();
assert!(constraint.test(&Array::from([0.5, -9.0])));
assert!(!constraint.test(&Array::from([-0.5, 0.0])));
}
#[test]
fn private_constraint_slices_bounds_per_argument() {
let constraint = two_argument_model().constraint();
let lower = constraint.lower_bound(&Array::from([0.5, 0.5]));
assert_eq!(lower[0], 0.0);
assert_eq!(lower[1], -crate::types::Real::MAX);
}
#[test]
fn set_params_notifies_registered_observers() {
let mut model = two_argument_model();
let counter = shared_mut(UpdateCounter { count: 0 });
model
.observable()
.register_observer(&(counter.clone() as SharedMut<dyn Observer>));
model.set_params(&Array::from([0.3, 0.4])).unwrap();
assert_eq!(counter.borrow().count, 1);
}
#[test]
fn as_observer_rebroadcasts_to_the_models_observers() {
let model = CalibratedModel::new(0);
let counter = shared_mut(UpdateCounter { count: 0 });
model
.observable()
.register_observer(&(counter.clone() as SharedMut<dyn Observer>));
model.as_observer().borrow_mut().update();
assert_eq!(counter.borrow().count, 1);
}
#[test]
fn write_params_updates_the_arguments_without_notifying() {
let mut model = two_argument_model();
let counter = shared_mut(UpdateCounter { count: 0 });
model
.observable()
.register_observer(&(counter.clone() as SharedMut<dyn Observer>));
model.write_params(&Array::from([0.3, 0.4])).unwrap();
assert_eq!(model.params(), Array::from([0.3, 0.4]));
assert_eq!(counter.borrow().count, 0);
}
struct FittedModel {
model: CalibratedModel,
regenerations: usize,
}
impl CalibratedModelHolder for FittedModel {
fn calibrated_model(&self) -> &CalibratedModel {
&self.model
}
fn calibrated_model_mut(&mut self) -> &mut CalibratedModel {
&mut self.model
}
fn generate_arguments(&mut self) {
self.regenerations += 1;
}
}
#[test]
fn holder_set_params_writes_regenerates_then_notifies() {
let mut fitted = FittedModel {
model: two_argument_model(),
regenerations: 0,
};
let counter = shared_mut(UpdateCounter { count: 0 });
fitted
.calibrated_model()
.observable()
.register_observer(&(counter.clone() as SharedMut<dyn Observer>));
fitted.set_params(&Array::from([0.3, 0.4])).unwrap();
assert_eq!(fitted.calibrated_model().params(), Array::from([0.3, 0.4]));
assert_eq!(fitted.regenerations, 1);
assert_eq!(counter.borrow().count, 1);
}
#[test]
fn holder_set_params_propagates_the_size_error() {
let mut fitted = FittedModel {
model: two_argument_model(),
regenerations: 0,
};
let err = fitted.set_params(&Array::from([0.3])).unwrap_err();
assert_eq!(err.message(), "parameter array too small");
assert_eq!(fitted.regenerations, 0);
}
#[test]
fn term_structure_consistent_model_exposes_its_handle() {
let curve = FlatForward::with_rate(
Date::new(17, Month::May, 1998),
0.05,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
);
let handle: Handle<dyn YieldTermStructure> =
Handle::new(shared(curve) as Shared<dyn YieldTermStructure>);
let consistent = TermStructureConsistentModel::new(handle.clone());
assert!(consistent.term_structure().points_to_same_link(&handle));
}
fn flat_curve(rate: Real) -> Shared<dyn YieldTermStructure> {
shared(FlatForward::with_rate(
Date::new(17, Month::May, 1998),
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>
}
struct FittedScalarModel {
model: CalibratedModel,
term_structure: Handle<dyn YieldTermStructure>,
discount_at_one: Real,
}
impl FittedScalarModel {
fn new(term_structure: Handle<dyn YieldTermStructure>) -> FittedScalarModel {
let mut fitted = FittedScalarModel {
model: CalibratedModel::new(0),
term_structure,
discount_at_one: 0.0,
};
fitted.generate_arguments();
fitted
}
}
impl CalibratedModelHolder for FittedScalarModel {
fn calibrated_model(&self) -> &CalibratedModel {
&self.model
}
fn calibrated_model_mut(&mut self) -> &mut CalibratedModel {
&mut self.model
}
fn generate_arguments(&mut self) {
self.discount_at_one = self
.term_structure
.current_link()
.unwrap()
.discount(1.0, true)
.unwrap();
}
}
#[test]
fn registered_model_regenerates_on_relink() {
let curve2 = flat_curve(0.10);
let rh: RelinkableHandle<dyn YieldTermStructure> = RelinkableHandle::new(flat_curve(0.05));
let model = shared_mut(FittedScalarModel::new(rh.handle()));
let before = model.borrow().discount_at_one;
let _observer = register_with_term_structure(&model, &rh.handle());
rh.link_to(curve2.clone());
let after = model.borrow().discount_at_one;
assert_ne!(before, after, "relink must regenerate the cached scalar");
assert_eq!(
after,
curve2.discount(1.0, true).unwrap(),
"the regenerated scalar must reflect the newly linked curve"
);
}
#[test]
fn unregistered_model_does_not_regenerate_on_relink() {
let rh: RelinkableHandle<dyn YieldTermStructure> = RelinkableHandle::new(flat_curve(0.05));
let model = shared_mut(FittedScalarModel::new(rh.handle()));
let before = model.borrow().discount_at_one;
rh.link_to(flat_curve(0.10));
assert_eq!(
model.borrow().discount_at_one,
before,
"an unregistered model must not regenerate on relink"
);
}
struct ScalarReader {
model: WeakMut<FittedScalarModel>,
seen: Option<Real>,
}
impl Observer for ScalarReader {
fn update(&mut self) {
self.seen = self.model.upgrade().map(|m| m.borrow().discount_at_one);
}
}
#[test]
fn model_observers_see_the_regenerated_value_during_notification() {
let curve2 = flat_curve(0.10);
let rh: RelinkableHandle<dyn YieldTermStructure> = RelinkableHandle::new(flat_curve(0.05));
let model = shared_mut(FittedScalarModel::new(rh.handle()));
let _observer = register_with_term_structure(&model, &rh.handle());
let reader = shared_mut(ScalarReader {
model: SharedMut::downgrade(&model),
seen: None,
});
model
.borrow()
.calibrated_model()
.observable()
.register_observer(&(reader.clone() as SharedMut<dyn Observer>));
rh.link_to(curve2.clone());
assert_eq!(
reader.borrow().seen,
Some(curve2.discount(1.0, true).unwrap())
);
}
struct DerivedModel {
model: CalibratedModel,
derived: Rc<Cell<Real>>,
}
impl DerivedModel {
fn new(seed: Real, derived: Rc<Cell<Real>>) -> SharedMut<DerivedModel> {
let mut model = CalibratedModel::new(1);
model.arguments_mut()[0] = ConstantParameter::new(seed, Rc::new(NoConstraint)).unwrap();
let mut fitted = DerivedModel { model, derived };
fitted.generate_arguments();
shared_mut(fitted)
}
}
impl CalibratedModelHolder for DerivedModel {
fn calibrated_model(&self) -> &CalibratedModel {
&self.model
}
fn calibrated_model_mut(&mut self) -> &mut CalibratedModel {
&mut self.model
}
fn generate_arguments(&mut self) {
self.derived.set(2.0 * self.model.params()[0]);
}
}
struct DerivedHelper {
derived: Rc<Cell<Real>>,
target: Real,
}
impl CalibrationHelper for DerivedHelper {
fn calibration_error(&mut self) -> QlResult<Real> {
Ok(self.derived.get() - self.target)
}
}
fn helper(derived: &Rc<Cell<Real>>, target: Real) -> SharedMut<dyn CalibrationHelper> {
shared_mut(DerivedHelper {
derived: Rc::clone(derived),
target,
}) as SharedMut<dyn CalibrationHelper>
}
fn criteria() -> EndCriteria {
EndCriteria::new(1000, Some(100), 1e-10, 1e-10, None).unwrap()
}
#[test]
fn calibrate_drives_the_free_parameter_to_the_analytic_minimum() {
let derived = Rc::new(Cell::new(0.0));
let model = DerivedModel::new(1.0, Rc::clone(&derived));
let instruments = vec![helper(&derived, 6.0)];
let mut method = LevenbergMarquardt::default();
calibrate(
&model,
&instruments,
&mut method,
&criteria(),
None,
Vec::new(),
Vec::new(),
)
.unwrap();
assert!((model.borrow().calibrated_model().params()[0] - 3.0).abs() < 1e-4);
assert!((derived.get() - 6.0).abs() < 1e-4);
assert!(model.borrow().calibrated_model().end_criteria().succeeded());
}
#[test]
fn calibrate_finds_the_least_squares_point_of_two_helpers() {
let derived = Rc::new(Cell::new(0.0));
let model = DerivedModel::new(1.0, Rc::clone(&derived));
let instruments = vec![helper(&derived, 6.0), helper(&derived, 10.0)];
let mut method = LevenbergMarquardt::default();
calibrate(
&model,
&instruments,
&mut method,
&criteria(),
None,
Vec::new(),
Vec::new(),
)
.unwrap();
assert!((model.borrow().calibrated_model().params()[0] - 4.0).abs() < 1e-4);
}
#[test]
fn calibrate_rejects_an_all_fixed_projection() {
let derived = Rc::new(Cell::new(0.0));
let model = DerivedModel::new(1.0, Rc::clone(&derived));
let instruments = vec![helper(&derived, 6.0)];
let mut method = LevenbergMarquardt::default();
let err = calibrate(
&model,
&instruments,
&mut method,
&criteria(),
None,
Vec::new(),
vec![true],
)
.unwrap_err();
assert_eq!(err.message(), "numberOfFreeParameters==0");
}
#[test]
fn calibrate_guards_reject_bad_inputs() {
let derived = Rc::new(Cell::new(0.0));
let model = DerivedModel::new(1.0, Rc::clone(&derived));
let mut method = LevenbergMarquardt::default();
let err = calibrate(
&model,
&[],
&mut method,
&criteria(),
None,
Vec::new(),
Vec::new(),
)
.unwrap_err();
assert_eq!(err.message(), "no instruments provided");
let instruments = vec![helper(&derived, 6.0)];
let err = calibrate(
&model,
&instruments,
&mut method,
&criteria(),
None,
vec![1.0, 1.0],
Vec::new(),
)
.unwrap_err();
assert_eq!(
err.message(),
"mismatch between number of instruments (1) and weights (2)"
);
let err = calibrate(
&model,
&instruments,
&mut method,
&criteria(),
None,
Vec::new(),
vec![false, false],
)
.unwrap_err();
assert_eq!(
err.message(),
"mismatch between number of parameters (1) and fixed-parameter specs (2)"
);
}
#[test]
fn calibration_value_is_the_root_sum_of_squares_and_writes_params() {
let derived = Rc::new(Cell::new(0.0));
let model = DerivedModel::new(1.0, Rc::clone(&derived));
let instruments = vec![helper(&derived, 6.0), helper(&derived, 10.0)];
let value = calibration_value(&model, &Array::from([2.0]), &instruments).unwrap();
assert!((value - 40.0_f64.sqrt()).abs() < 1e-12);
assert_eq!(model.borrow().calibrated_model().params()[0], 2.0);
assert!((derived.get() - 4.0).abs() < 1e-12);
}
}