use std::rc::Rc;
use crate::errors::QlResult;
use crate::handle::Handle;
use crate::interestrate::Compounding;
use crate::math::array::Array;
use crate::models::model::{CalibratedModel, CalibratedModelHolder, TermStructureConsistentModel};
use crate::models::parameter::{
NullParameter, Parameter, ParameterValue, TermStructureFittingParameter,
};
use crate::models::shortrate::coxingersollross::CoxIngersollRoss;
use crate::models::shortrate::onefactormodel::OneFactorAffineModel;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::frequency::Frequency;
use crate::types::{Rate, Real, Time};
struct FittingParameterValue {
term_structure: Handle<dyn YieldTermStructure>,
theta: Real,
k: Real,
sigma: Real,
x0: Real,
}
impl ParameterValue for FittingParameterValue {
fn value(&self, _params: &Array, t: Time) -> Real {
let curve = self
.term_structure
.current_link()
.expect("the extended CIR fitting law requires a non-empty term-structure handle");
let forward = curve
.forward_rate(t, t, Compounding::Continuous, Frequency::NoFrequency, false)
.expect("the extended CIR fitting law's forward rate is well-defined on its curve")
.rate();
let h = (self.k * self.k + 2.0 * self.sigma * self.sigma).sqrt();
let expth = (t * h).exp();
let temp = 2.0 * h + (self.k + h) * (expth - 1.0);
forward
- 2.0 * self.k * self.theta * (expth - 1.0) / temp
- self.x0 * 4.0 * h * h * expth / (temp * temp)
}
}
pub struct ExtendedCoxIngersollRoss {
base: CoxIngersollRoss,
ts_model: TermStructureConsistentModel,
phi: Parameter,
}
impl ExtendedCoxIngersollRoss {
pub fn new(
term_structure: Handle<dyn YieldTermStructure>,
theta: Real,
k: Real,
sigma: Real,
x0: Rate,
with_feller_constraint: bool,
) -> QlResult<ExtendedCoxIngersollRoss> {
let base = CoxIngersollRoss::new(x0, theta, k, sigma, with_feller_constraint)?;
let ts_model = TermStructureConsistentModel::new(term_structure);
let mut model = ExtendedCoxIngersollRoss {
base,
ts_model,
phi: NullParameter::new(),
};
model.generate_arguments();
Ok(model)
}
}
impl CalibratedModelHolder for ExtendedCoxIngersollRoss {
fn calibrated_model(&self) -> &CalibratedModel {
self.base.calibrated_model()
}
fn calibrated_model_mut(&mut self) -> &mut CalibratedModel {
self.base.calibrated_model_mut()
}
fn generate_arguments(&mut self) {
let law = FittingParameterValue {
term_structure: self.ts_model.term_structure().clone(),
theta: self.base.theta(),
k: self.base.k(),
sigma: self.base.sigma(),
x0: self.base.x0(),
};
self.phi = TermStructureFittingParameter::new(Rc::new(law));
}
}
impl OneFactorAffineModel for ExtendedCoxIngersollRoss {
fn a(&self, t: Time, s: Time) -> Real {
let curve = self
.ts_model
.term_structure()
.current_link()
.expect("the extended CIR model requires a non-empty term-structure handle");
let pt = curve
.discount(t, false)
.expect("the extended CIR model's discount is well-defined on its curve");
let ps = curve
.discount(s, false)
.expect("the extended CIR model's discount is well-defined on its curve");
let x0 = self.base.x0();
let base_a_ts = self.base.a(t, s);
let b_ts = self.base.b(t, s);
let phi_t = self.phi.value(t);
let base_a_0t = self.base.a(0.0, t);
let base_a_0s = self.base.a(0.0, s);
let b_0t = self.base.b(0.0, t);
let b_0s = self.base.b(0.0, s);
base_a_ts * (b_ts * phi_t).exp() * (ps * base_a_0t * (-b_0t * x0).exp())
/ (pt * base_a_0s * (-b_0s * x0).exp())
}
fn b(&self, t: Time, maturity: Time) -> Real {
self.base.b(t, maturity)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::array::Array;
use crate::math::interpolations::linear::Linear;
use crate::shared::{Shared, shared};
use crate::termstructures::yields::{FlatForward, ZeroCurve};
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual365fixed::Actual365Fixed;
fn flat_curve(rate: Rate) -> Handle<dyn YieldTermStructure> {
let curve = FlatForward::with_rate(
Date::new(15, Month::January, 2026),
rate,
Actual365Fixed::new(),
Compounding::Continuous,
Frequency::Annual,
);
Handle::new(shared(curve) as Shared<dyn YieldTermStructure>)
}
fn sloped_curve() -> Handle<dyn YieldTermStructure> {
let reference = Date::new(15, Month::January, 2026);
let dates = vec![
reference,
reference + 365,
reference + 730,
reference + 1095,
];
let zeros = vec![0.03, 0.04, 0.05, 0.055];
let curve = ZeroCurve::new(dates, zeros, Actual365Fixed::new(), Linear).unwrap();
Handle::new(shared(curve) as Shared<dyn YieldTermStructure>)
}
#[test]
fn extended_cir_reproduces_the_curve_forward_discount() {
let rate = 0.1;
let handle = flat_curve(rate);
let model =
ExtendedCoxIngersollRoss::new(handle.clone(), rate, 1.0, 1e-4, rate, true).unwrap();
let curve = handle.current_link().unwrap();
let expected = curve.discount(2.5, false).unwrap() / curve.discount(1.5, false).unwrap();
let calculated = model.discount_bond(1.5, 2.5, rate);
assert!((expected - calculated).abs() < 1e-6);
}
#[test]
fn discount_bond_matches_cpp_on_a_sloped_curve() {
let handle = sloped_curve();
let curve = handle.current_link().unwrap();
let model =
ExtendedCoxIngersollRoss::new(handle.clone(), 0.05, 0.5, 0.03, 0.05, true).unwrap();
let base = CoxIngersollRoss::new(0.05, 0.05, 0.5, 0.03, true).unwrap();
let f0_cpp = 0.030_001_000_000_790_656;
let f0 = curve
.forward_rate(
0.0,
0.0,
Compounding::Continuous,
Frequency::NoFrequency,
false,
)
.unwrap()
.rate();
assert!((f0 - f0_cpp).abs() < 1e-12);
let reprice = [
(0.5, 0.982_652_235_665_073_2),
(1.0, 0.960_789_439_152_323_2),
(1.5, 0.934_727_720_616_027_5),
(2.0, 0.904_837_418_035_959_5),
(3.0, 0.847_893_704_087_915_9),
];
for (t, p_cpp) in reprice {
assert!((curve.discount(t, false).unwrap() - p_cpp).abs() < 1e-14);
assert!((model.discount_bond(0.0, t, f0) - p_cpp).abs() < 1e-8);
assert!((base.discount_bond(0.0, t, f0) - p_cpp).abs() > 1e-3);
}
let ext_t_cpp = 0.903_817_675_549_668_5;
assert!((model.discount_bond(0.5, 2.5, f0) - ext_t_cpp).abs() < 1e-8);
assert!((base.discount_bond(0.5, 2.5, f0) - ext_t_cpp).abs() > 1e-3);
}
#[test]
fn a_override_reprices_the_curve_where_base_cir_does_not() {
let rate = 0.08;
let handle = flat_curve(rate);
let extended =
ExtendedCoxIngersollRoss::new(handle.clone(), 0.04, 0.3, 0.02, 0.05, true).unwrap();
let base = CoxIngersollRoss::new(0.05, 0.04, 0.3, 0.02, true).unwrap();
let p2 = handle.current_link().unwrap().discount(2.0, false).unwrap();
let extended_db = extended.discount_bond(0.0, 2.0, rate);
let base_db = base.discount_bond(0.0, 2.0, rate);
assert!((extended_db - p2).abs() < 1e-9);
assert!((base_db - p2).abs() > 1e-3);
}
#[test]
fn set_params_rebuilds_phi_and_changes_the_price() {
let rate = 0.08;
let handle = flat_curve(rate);
let mut model =
ExtendedCoxIngersollRoss::new(handle.clone(), 0.04, 0.3, 0.02, 0.05, true).unwrap();
let before = model.discount_bond(1.5, 2.5, rate);
model
.set_params(&Array::from([0.04, 0.6, 0.02, 0.07]))
.unwrap();
let after = model.discount_bond(1.5, 2.5, rate);
assert!((before - after).abs() > 1e-6);
}
}