use std::rc::Rc;
use crate::errors::QlResult;
use crate::math::array::Array;
use crate::math::optimization::constraint::{BoundaryConstraint, Constraint, PositiveConstraint};
use crate::models::model::{CalibratedModel, CalibratedModelHolder, register_with_term_structure};
use crate::models::parameter::ConstantParameter;
use crate::patterns::observable::Observer;
use crate::processes::HestonProcess;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::types::Real;
pub struct HestonModel {
model: CalibratedModel,
process: Shared<HestonProcess>,
#[allow(dead_code)]
observer: Option<SharedMut<dyn Observer>>,
}
impl HestonModel {
pub fn new(process: Shared<HestonProcess>) -> QlResult<SharedMut<HestonModel>> {
let mut model = CalibratedModel::new(5);
model.arguments_mut()[0] =
ConstantParameter::new(process.theta(), Rc::new(PositiveConstraint))?;
model.arguments_mut()[1] =
ConstantParameter::new(process.kappa(), Rc::new(PositiveConstraint))?;
model.arguments_mut()[2] =
ConstantParameter::new(process.sigma(), Rc::new(PositiveConstraint))?;
model.arguments_mut()[3] =
ConstantParameter::new(process.rho(), Rc::new(BoundaryConstraint::new(-1.0, 1.0)))?;
model.arguments_mut()[4] =
ConstantParameter::new(process.v0(), Rc::new(PositiveConstraint))?;
let risk_free_rate = process.risk_free_rate();
let dividend_yield = process.dividend_yield();
let s0 = process.s0();
let mut heston = HestonModel {
model,
process,
observer: None,
};
heston.generate_arguments();
let shared = shared_mut(heston);
let observer = register_with_term_structure(&shared, &risk_free_rate);
dividend_yield.register_observer(&observer);
s0.register_observer(&observer);
shared.borrow_mut().observer = Some(observer);
Ok(shared)
}
pub fn theta(&self) -> Real {
self.model.arguments()[0].value(0.0)
}
pub fn kappa(&self) -> Real {
self.model.arguments()[1].value(0.0)
}
pub fn sigma(&self) -> Real {
self.model.arguments()[2].value(0.0)
}
pub fn rho(&self) -> Real {
self.model.arguments()[3].value(0.0)
}
pub fn v0(&self) -> Real {
self.model.arguments()[4].value(0.0)
}
pub fn process(&self) -> Shared<HestonProcess> {
self.process.clone()
}
}
impl CalibratedModelHolder for HestonModel {
fn calibrated_model(&self) -> &CalibratedModel {
&self.model
}
fn calibrated_model_mut(&mut self) -> &mut CalibratedModel {
&mut self.model
}
fn generate_arguments(&mut self) {
self.process = shared(HestonProcess::new(
self.process.risk_free_rate(),
self.process.dividend_yield(),
self.process.s0(),
self.v0(),
self.kappa(),
self.theta(),
self.sigma(),
self.rho(),
));
}
}
pub struct FellerConstraint;
impl Constraint for FellerConstraint {
fn test(&self, params: &Array) -> bool {
let theta = params[0];
let kappa = params[1];
let sigma = params[2];
sigma >= 0.0 && sigma * sigma < 2.0 * kappa * theta
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::{Handle, RelinkableHandle};
use crate::interestrate::Compounding;
use crate::quotes::make_quote_handle;
use crate::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;
use crate::types::Rate;
const S0: Real = 100.0;
const V0: Real = 0.04;
const KAPPA: Real = 1.2;
const THETA: Real = 0.06;
const SIGMA: Real = 0.3;
const RHO: Real = -0.5;
const R: Rate = 0.05;
const Q: Rate = 0.02;
fn reference() -> Date {
Date::new(15, Month::June, 2026)
}
fn flat_yield(rate: Rate) -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
reference(),
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
fn process_on(risk_free: Handle<dyn YieldTermStructure>) -> Shared<HestonProcess> {
shared(HestonProcess::new(
risk_free,
flat_yield(Q),
make_quote_handle(S0).handle(),
V0,
KAPPA,
THETA,
SIGMA,
RHO,
))
}
fn make_model() -> SharedMut<HestonModel> {
HestonModel::new(process_on(flat_yield(R))).unwrap()
}
#[test]
fn ctor_round_trips_params_and_rebuilt_process() {
let model = make_model();
let m = model.borrow();
assert_eq!(m.theta(), THETA);
assert_eq!(m.kappa(), KAPPA);
assert_eq!(m.sigma(), SIGMA);
assert_eq!(m.rho(), RHO);
assert_eq!(m.v0(), V0);
let process = m.process();
assert_eq!(process.theta(), THETA);
assert_eq!(process.kappa(), KAPPA);
assert_eq!(process.sigma(), SIGMA);
assert_eq!(process.rho(), RHO);
assert_eq!(process.v0(), V0);
}
#[test]
fn set_params_rebuilds_the_process() {
let model = make_model();
let new_theta = 0.07;
let new_kappa = 1.5;
let new_sigma = 0.35;
let new_rho = -0.6;
let new_v0 = 0.05;
model
.borrow_mut()
.set_params(&Array::from([
new_theta, new_kappa, new_sigma, new_rho, new_v0,
]))
.unwrap();
let m = model.borrow();
assert_eq!(m.sigma(), new_sigma);
let process = m.process();
assert_eq!(process.theta(), new_theta);
assert_eq!(process.kappa(), new_kappa);
assert_eq!(process.sigma(), new_sigma);
assert_eq!(process.rho(), new_rho);
assert_eq!(process.v0(), new_v0);
}
#[test]
fn relink_rebuilds_the_process() {
let rh: RelinkableHandle<dyn YieldTermStructure> =
RelinkableHandle::new(shared(FlatForward::with_rate(
reference(),
R,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
let model = HestonModel::new(process_on(rh.handle())).unwrap();
let before = model.borrow().process();
rh.link_to(shared(FlatForward::with_rate(
reference(),
0.08,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>);
let after = model.borrow().process();
assert!(
!Shared::ptr_eq(&before, &after),
"a relink must rebuild the process"
);
assert_eq!(after.sigma(), SIGMA);
assert_eq!(after.v0(), V0);
}
#[test]
fn feller_constraint_tests_the_variance_positivity_condition() {
let c = FellerConstraint;
assert!(c.test(&Array::from([THETA, KAPPA, SIGMA, RHO, V0])));
assert!(!c.test(&Array::from([THETA, KAPPA, 0.5, RHO, V0])));
assert!(!c.test(&Array::from([THETA, KAPPA, -0.1, RHO, V0])));
}
}