mod coefficients;
mod moments;
#[cfg(test)]
mod oracle_values;
#[cfg(test)]
mod tests;
use crate::errors::QlResult;
use crate::exercise::ExerciseType;
use crate::instruments::{
OneAssetOptionEngine, OneAssetOptionResults, OptionArguments, PlainVanillaPayoff,
StrikedTypePayoff, TypePayoff,
};
use crate::math::distributions::normal::CumulativeNormalDistribution;
use crate::models::GjrGarchModel;
use crate::models::model::CalibratedModelHolder;
use crate::option::OptionType;
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::{Arguments, PricingEngine, Results};
use crate::shared::SharedMut;
use crate::types::Real;
use crate::{fail, require};
use coefficients::Coefficients;
use moments::Moments;
use std::any::Any;
pub struct AnalyticGjrGarchEngine {
base: OneAssetOptionEngine,
model: SharedMut<GjrGarchModel>,
moments: Option<(MomentKey, Moments)>,
}
#[derive(Clone, Copy, PartialEq)]
struct MomentKey {
parameters: [Real; 6],
days: usize,
rate: Real,
}
impl AnalyticGjrGarchEngine {
pub fn new(model: SharedMut<GjrGarchModel>) -> Self {
let base =
OneAssetOptionEngine::new(OptionArguments::default(), OneAssetOptionResults::default());
base.register_with(model.borrow().calibrated_model().observable());
Self {
base,
model,
moments: None,
}
}
}
impl AsObservable for AnalyticGjrGarchEngine {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl PricingEngine for AnalyticGjrGarchEngine {
fn arguments_mut(&mut self) -> &mut dyn Arguments {
self.base.arguments_mut()
}
fn results(&self) -> &dyn Results {
self.base.results()
}
fn reset(&mut self) {
self.base.reset();
}
fn calculate(&mut self) -> QlResult<()> {
let arguments = self.base.arguments();
let Some(exercise) = &arguments.exercise else {
fail!("no exercise given");
};
require!(
exercise.exercise_type() == ExerciseType::European,
"AnalyticGjrGarchEngine requires European exercise"
);
let Some(payoff) = &arguments.payoff else {
fail!("no payoff given");
};
let Some(payoff) = (&**payoff as &dyn Any).downcast_ref::<PlainVanillaPayoff>() else {
fail!("AnalyticGjrGarchEngine requires plain-vanilla payoff");
};
let strike = payoff.strike();
let option_type = payoff.option_type();
let maturity = exercise.last_date();
require!(
strike.is_finite() && strike > 0.0,
"GJR-GARCH analytic strike must be finite and positive"
);
let process = self.model.borrow().process();
let spot = process.s0().current_link()?.value()?;
require!(
spot.is_finite() && spot > 0.0,
"GJR-GARCH analytic spot must be finite and positive"
);
let risk_free_discount = process
.risk_free_rate()
.current_link()?
.discount_date(maturity, false)?;
let dividend_discount = process
.dividend_yield()
.current_link()?
.discount_date(maturity, false)?;
require!(
risk_free_discount.is_finite()
&& risk_free_discount > 0.0
&& dividend_discount.is_finite()
&& dividend_discount > 0.0,
"GJR-GARCH analytic discounts must be finite and positive"
);
let term = process.time(&maturity)?;
let model_days = process.days_per_year() * term;
let rounded_days = model_days.round();
require!(
term.is_finite()
&& term > 0.0
&& model_days.is_finite()
&& (1.0..=1000.0).contains(&rounded_days),
"GJR-GARCH analytic rounded daily horizon must be between 1 and 1000"
);
let days = rounded_days as usize;
let discount_ratio = risk_free_discount / dividend_discount;
let rate = -discount_ratio.ln() / model_days;
require!(
discount_ratio.is_finite() && discount_ratio > 0.0 && rate.is_finite(),
"GJR-GARCH analytic daily rate is not finite"
);
let parameters = [
process.omega(),
process.alpha(),
process.beta(),
process.gamma(),
process.lambda(),
process.v0(),
];
let key = MomentKey {
parameters,
days,
rate,
};
let moments = match self.moments {
Some((cached_key, cached_moments)) if cached_key == key => cached_moments,
_ => {
let c =
Coefficients::new(parameters[2], parameters[1], parameters[3], parameters[4])?;
let moments = Moments::new(c, parameters[0], parameters[5], days, rate)?;
self.moments = Some((key, moments));
moments
}
};
let call = approximate_call(moments, spot, strike, rate * days as Real)?;
let value = match option_type {
OptionType::Call => call,
OptionType::Put => call + strike * discount_ratio - spot,
};
require!(value.is_finite(), "GJR-GARCH analytic price is not finite");
self.base.results_mut().instrument.value = Some(value);
Ok(())
}
}
fn approximate_call(m: Moments, spot: Real, strike: Real, drift: Real) -> QlResult<Real> {
let stdev = m.variance.sqrt();
let del = (m.mean - drift + m.variance / 2.0) / stdev;
let d = ((spot / strike).ln() + drift + m.variance / 2.0) / stdev;
let shifted_d = d + del;
let cdf = CumulativeNormalDistribution::standard();
let density = (-shifted_d * shifted_d / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt();
let asset = spot * (del * stdev).exp();
let call =
asset * cdf.value(shifted_d) - strike * (-drift).exp() * cdf.value(shifted_d - stdev);
let a3 =
asset * stdev * ((2.0 * stdev - shifted_d) * density + m.variance * cdf.value(shifted_d))
/ 6.0;
let a4 = asset
* stdev
* ((shifted_d * shifted_d - 1.0 - 3.0 * stdev * (shifted_d - stdev)) * density
- m.variance * stdev * cdf.value(shifted_d))
/ 24.0;
let value = call + m.skewness * a3 + (m.kurtosis - 3.0) * a4;
require!(
[del, d, shifted_d, asset, call, a3, a4, value]
.iter()
.all(|x| x.is_finite()),
"GJR-GARCH analytic expansion is not finite"
);
Ok(value)
}