#![allow(clippy::new_ret_no_self)]
use std::cell::RefCell;
use std::rc::Rc;
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::math::array::Array;
use crate::math::optimization::constraint::{Constraint, NoConstraint};
use crate::require;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::types::{Real, Size, Time};
pub trait ParameterValue {
fn value(&self, params: &Array, t: Time) -> Real;
}
#[derive(Clone)]
enum ParameterImpl {
Constant,
Null,
Custom(Rc<dyn ParameterValue>),
}
impl ParameterImpl {
fn value(&self, params: &Array, t: Time) -> Real {
match self {
ParameterImpl::Constant => params[0],
ParameterImpl::Null => 0.0,
ParameterImpl::Custom(impl_) => impl_.value(params, t),
}
}
}
#[derive(Clone)]
pub struct Parameter {
params: Array,
constraint: Rc<dyn Constraint>,
impl_: ParameterImpl,
}
impl Parameter {
pub fn params(&self) -> &Array {
&self.params
}
pub fn set_param(&mut self, i: Size, x: Real) {
self.params[i] = x;
}
pub fn test_params(&self, params: &Array) -> bool {
self.constraint.test(params)
}
pub fn size(&self) -> Size {
self.params.size()
}
pub fn value(&self, t: Time) -> Real {
self.impl_.value(&self.params, t)
}
pub fn constraint(&self) -> &dyn Constraint {
&*self.constraint
}
}
impl Default for Parameter {
fn default() -> Self {
Parameter {
params: Array::new(),
constraint: Rc::new(NoConstraint),
impl_: ParameterImpl::Null,
}
}
}
pub struct ConstantParameter;
impl ConstantParameter {
pub fn unset(constraint: Rc<dyn Constraint>) -> Parameter {
Parameter {
params: Array::with_size(1),
constraint,
impl_: ParameterImpl::Constant,
}
}
pub fn new(value: Real, constraint: Rc<dyn Constraint>) -> QlResult<Parameter> {
let mut params = Array::with_size(1);
params[0] = value;
let parameter = Parameter {
params,
constraint,
impl_: ParameterImpl::Constant,
};
require!(
parameter.test_params(parameter.params()),
"{value}: invalid value"
);
Ok(parameter)
}
}
pub struct NullParameter;
impl NullParameter {
pub fn new() -> Parameter {
Parameter {
params: Array::new(),
constraint: Rc::new(NoConstraint),
impl_: ParameterImpl::Null,
}
}
}
pub struct TermStructureFittingParameter;
impl TermStructureFittingParameter {
pub fn new(impl_: Rc<dyn ParameterValue>) -> Parameter {
Parameter {
params: Array::new(),
constraint: Rc::new(NoConstraint),
impl_: ParameterImpl::Custom(impl_),
}
}
}
pub struct NumericalImpl {
nodes: RefCell<Vec<(Time, Real)>>,
term_structure: Handle<dyn YieldTermStructure>,
}
impl NumericalImpl {
pub fn new(term_structure: Handle<dyn YieldTermStructure>) -> Rc<Self> {
Rc::new(NumericalImpl {
nodes: RefCell::new(Vec::new()),
term_structure,
})
}
pub fn set(&self, t: Time, x: Real) {
self.nodes.borrow_mut().push((t, x));
}
pub fn reset(&self) {
self.nodes.borrow_mut().clear();
}
pub fn term_structure(&self) -> &Handle<dyn YieldTermStructure> {
&self.term_structure
}
}
impl ParameterValue for NumericalImpl {
fn value(&self, _params: &Array, t: Time) -> Real {
let nodes = self.nodes.borrow();
nodes
.iter()
.find(|(time, _)| *time == t)
.map(|(_, x)| *x)
.expect("fitting parameter not set!")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::Handle;
use crate::interestrate::Compounding;
use crate::math::optimization::constraint::PositiveConstraint;
use crate::shared::{Shared, shared};
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
#[test]
fn constant_parameter_returns_its_value_at_any_time() {
let p = ConstantParameter::new(0.42, Rc::new(NoConstraint)).unwrap();
assert_eq!(p.size(), 1);
assert_eq!(p.value(0.0), 0.42);
assert_eq!(p.value(10.0), 0.42);
assert_eq!(p.params()[0], 0.42);
}
#[test]
fn constant_parameter_rejects_a_value_the_constraint_forbids() {
let err = ConstantParameter::new(-1.0, Rc::new(PositiveConstraint))
.err()
.unwrap();
assert_eq!(err.message(), "-1: invalid value");
}
#[test]
fn unset_constant_parameter_defaults_to_zero() {
let p = ConstantParameter::unset(Rc::new(NoConstraint));
assert_eq!(p.size(), 1);
assert_eq!(p.value(0.0), 0.0);
}
#[test]
fn null_parameter_is_always_zero_and_sizeless() {
let p = NullParameter::new();
assert_eq!(p.size(), 0);
assert_eq!(p.value(0.0), 0.0);
assert_eq!(p.value(5.0), 0.0);
}
#[test]
fn set_param_updates_the_stored_value() {
let mut p = ConstantParameter::unset(Rc::new(NoConstraint));
p.set_param(0, 0.03);
assert_eq!(p.value(0.0), 0.03);
}
#[test]
fn test_params_delegates_to_the_constraint() {
let p = ConstantParameter::unset(Rc::new(PositiveConstraint));
assert!(p.test_params(&Array::from([1.0])));
assert!(!p.test_params(&Array::from([-1.0])));
}
struct LinearLaw {
slope: Real,
}
impl ParameterValue for LinearLaw {
fn value(&self, _params: &Array, t: Time) -> Real {
self.slope * t
}
}
#[test]
fn fitting_parameter_wraps_a_time_dependent_state_capturing_law() {
let p = TermStructureFittingParameter::new(Rc::new(LinearLaw { slope: 0.02 }));
assert_eq!(p.size(), 0);
assert_eq!(p.value(0.0), 0.0);
assert!((p.value(3.0) - 0.06).abs() < 1e-15);
}
struct ForwardPlusSlope {
term_structure: Handle<dyn YieldTermStructure>,
slope: Real,
}
impl ParameterValue for ForwardPlusSlope {
fn value(&self, _params: &Array, t: Time) -> Real {
let curve = self
.term_structure
.current_link()
.expect("a fitting parameter requires a non-empty term-structure handle");
let forward = curve
.forward_rate(t, t, Compounding::Continuous, Frequency::NoFrequency, false)
.expect("a fitting parameter's forward rate is well-defined on its curve")
.rate();
forward + self.slope * t
}
}
#[test]
fn fitting_parameter_reads_its_captured_term_structure_live() {
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 phi = TermStructureFittingParameter::new(Rc::new(ForwardPlusSlope {
term_structure: handle,
slope: 0.01,
}));
assert_eq!(phi.size(), 0);
assert!((phi.value(2.0) - (0.05 + 0.01 * 2.0)).abs() < 1e-9);
assert!((phi.value(0.5) - (0.05 + 0.01 * 0.5)).abs() < 1e-9);
}
fn flat_handle() -> Handle<dyn YieldTermStructure> {
let curve = FlatForward::with_rate(
Date::new(17, Month::May, 1998),
0.05,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
);
Handle::new(shared(curve) as Shared<dyn YieldTermStructure>)
}
#[test]
fn numerical_impl_set_then_value_round_trips_per_node() {
let impl_ = NumericalImpl::new(flat_handle());
impl_.set(1.0, 0.031);
impl_.set(2.0, 0.042);
impl_.set(3.0, 0.055);
assert_eq!(impl_.value(&Array::new(), 1.0), 0.031);
assert_eq!(impl_.value(&Array::new(), 2.0), 0.042);
assert_eq!(impl_.value(&Array::new(), 3.0), 0.055);
}
#[test]
#[should_panic(expected = "fitting parameter not set!")]
fn numerical_impl_value_at_an_unset_time_panics_with_the_cpp_message() {
let impl_ = NumericalImpl::new(flat_handle());
impl_.set(1.0, 0.03);
impl_.value(&Array::new(), 2.0);
}
#[test]
#[should_panic(expected = "fitting parameter not set!")]
fn numerical_impl_lookup_is_exact_not_tolerant() {
let impl_ = NumericalImpl::new(flat_handle());
impl_.set(1.0, 0.03);
impl_.value(&Array::new(), 1.0 + 1e-12);
}
#[test]
fn numerical_impl_reset_clears_every_node() {
let impl_ = NumericalImpl::new(flat_handle());
impl_.set(1.0, 0.03);
impl_.reset();
impl_.set(2.0, 0.07);
assert_eq!(impl_.value(&Array::new(), 2.0), 0.07);
}
#[test]
fn set_through_the_retained_rc_is_visible_through_the_type_erased_parameter() {
let impl_ = NumericalImpl::new(flat_handle());
let phi = TermStructureFittingParameter::new(impl_.clone() as Rc<dyn ParameterValue>);
impl_.set(2.5, 0.037);
assert_eq!(phi.value(2.5), 0.037);
}
}