use std::any::Any;
use crate::errors::QlResult;
use crate::exercise::ExerciseType;
use crate::instruments::{
OneAssetOptionEngine, OneAssetOptionResults, OptionArguments, PlainVanillaPayoff,
StrikedTypePayoff, TypePayoff,
};
use crate::math::expm1::{expm1, log1p};
use crate::math::integrals::exponential_integrals::{ci_complex, si_complex};
use crate::math::integrals::gaussianquadratures::GaussianQuadrature;
use crate::models::HestonModel;
use crate::models::model::CalibratedModelHolder;
use crate::option::OptionType;
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::{Arguments, PricingEngine, Results};
use crate::pricingengines::BlackCalculator;
use crate::shared::SharedMut;
use crate::stochasticprocess::StochasticProcess;
use crate::types::{Complex, Real, Size, Time};
use crate::{fail, require};
#[derive(Clone, Copy, Debug)]
pub struct HestonChf {
kappa: Real,
theta: Real,
sigma: Real,
rho: Real,
v0: Real,
}
impl HestonChf {
pub fn new(kappa: Real, theta: Real, sigma: Real, rho: Real, v0: Real) -> Self {
HestonChf {
kappa,
theta,
sigma,
rho,
v0,
}
}
pub fn kappa(&self) -> Real {
self.kappa
}
pub fn theta(&self) -> Real {
self.theta
}
pub fn sigma(&self) -> Real {
self.sigma
}
pub fn rho(&self) -> Real {
self.rho
}
pub fn v0(&self) -> Real {
self.v0
}
pub fn ln_chf(&self, z: Complex, t: Time) -> Complex {
let kappa = self.kappa;
let sigma = self.sigma;
let theta = self.theta;
let rho = self.rho;
let v0 = self.v0;
let sigma2 = sigma * sigma;
let g = Complex::new(kappa, 0.0) + rho * sigma * Complex::new(z.im, -z.re);
let d = (g * g + (z * z + Complex::new(-z.im, z.re)) * sigma2).sqrt();
let mut r = g - d;
if g.re * d.re + g.im * d.im > 0.0 {
r = -sigma2 * z * Complex::new(z.re, z.im + 1.0) / (g + d);
}
let y = if d.re != 0.0 || d.im != 0.0 {
expm1(-d * t) / (2.0 * d)
} else {
Complex::new(-0.5 * t, 0.0)
};
let a = kappa * theta / sigma2 * (r * t - 2.0 * log1p(-r * y));
let b = z * Complex::new(z.re, z.im + 1.0) * y / (Complex::new(1.0, 0.0) - r * y);
a + v0 * b
}
pub fn chf(&self, z: Complex, t: Time) -> Complex {
if self.sigma > 1e-6 || self.kappa < 1e-8 {
self.ln_chf(z, t).exp()
} else {
self.chf_small_sigma(z, t)
}
}
fn chf_small_sigma(&self, z: Complex, t: Time) -> Complex {
let kappa = self.kappa;
let sigma = self.sigma;
let theta = self.theta;
let rho = self.rho;
let v0 = self.v0;
let sigma2 = sigma * sigma;
let kappa2 = kappa * kappa;
let kt = kappa * t;
let ekt = kt.exp();
let e2kt = (2.0 * kt).exp();
let rho2 = rho * rho;
let zpi = z + Complex::new(0.0, 1.0);
let one_minus_iz = Complex::new(1.0, 0.0) - Complex::new(-z.im, z.re);
let a1 = theta - v0 + ekt * ((-1.0 + kt) * theta + v0);
let a2 = 2.0 * theta + kt * theta - v0 - kt * v0 + ekt * ((-2.0 + kt) * theta + v0);
let term0 = (-(a1 * z * zpi / ekt) / (2.0 * kappa)).exp();
let term1 =
(-kt - a1 * z * zpi / (2.0 * ekt * kappa)).exp() * rho * a2 * one_minus_iz * z * z
/ (2.0 * kappa2)
* sigma;
let s1 = -2.0 * rho2 * (a2 * a2) * z * z * zpi;
let s2 = 2.0
* kappa
* v0
* (-zpi + e2kt * (zpi + 4.0 * rho2 * z)
- 2.0 * ekt * (2.0 * rho2 * z + kt * (zpi + rho2 * (2.0 + kt) * z)));
let s3 = kappa
* theta
* (zpi
+ e2kt * (-5.0 * zpi - 24.0 * rho2 * z + 2.0 * kt * (zpi + 4.0 * rho2 * z))
+ 4.0 * ekt * (zpi + 6.0 * rho2 * z + kt * (zpi + rho2 * (4.0 + kt) * z)));
let term2 =
(-2.0 * kt - a1 * z * zpi / (2.0 * ekt * kappa)).exp() * z * z * zpi * (s1 + s2 + s3)
/ (16.0 * kappa2 * kappa2)
* sigma2;
term0 + term1 + term2
}
}
pub struct Integration {
quadrature: GaussianQuadrature,
}
impl Integration {
pub fn gauss_laguerre(int_order: Size) -> QlResult<Self> {
if int_order > 192 {
fail!("maximum integration order (192) exceeded: got {int_order}");
}
Ok(Integration {
quadrature: GaussianQuadrature::laguerre(int_order, 0.0)?,
})
}
pub fn number_of_evaluations(&self) -> Size {
self.quadrature.order()
}
pub fn is_adaptive_integration(&self) -> bool {
false
}
pub fn calculate<F: FnMut(Real) -> Real>(&self, _c_inf: Real, f: F) -> Real {
self.quadrature.integrate(f)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ComplexLogFormula {
AndersenPiterbarg,
AndersenPiterbargOptCV,
AsymptoticChF,
AngledContour,
AngledContourNoCV,
}
#[derive(Clone, Copy, Debug)]
pub struct ApHelper {
term: Time,
fwd: Real,
strike: Real,
freq: Real,
alpha: Real,
s_alpha: Real,
v_avg: Real,
tan_phi: Real,
phi: Complex,
psi: Complex,
cpx_log: ComplexLogFormula,
chf: HestonChf,
}
impl ApHelper {
pub fn new(
term: Time,
fwd: Real,
strike: Real,
cpx_log: ComplexLogFormula,
chf: HestonChf,
alpha: Real,
) -> QlResult<Self> {
let kappa = chf.kappa();
let theta = chf.theta();
let sigma = chf.sigma();
let rho = chf.rho();
let v0 = chf.v0();
let freq = (fwd / strike).ln();
let s_alpha = (alpha * freq).exp();
let (phi, psi) = match cpx_log {
ComplexLogFormula::AngledContour => (Complex::new(0.0, 0.0), Complex::new(0.0, 0.0)),
ComplexLogFormula::AsymptoticChF => {
let sqrt_1mrho2 = (1.0 - rho * rho).sqrt();
let phi = -(v0 + term * kappa * theta) / sigma * Complex::new(sqrt_1mrho2, rho);
let psi = Complex::new(
(kappa - 0.5 * rho * sigma) * (v0 + term * kappa * theta)
+ kappa * theta * (4.0 * (1.0 - rho * rho)).ln(),
-((0.5 * rho * rho * sigma - kappa * rho) / sqrt_1mrho2
* (v0 + kappa * theta * term)
- 2.0 * kappa * theta * (rho / sqrt_1mrho2).atan()),
) / (sigma * sigma);
(phi, psi)
}
other => fail!(
"AP_Helper control variate {other:?} is deferred (issue #418): only \
AngledContour (issue #416) and AsymptoticChF (issue #426) are ported; the \
deferred branches are not stubbed to a ported one because optimalControlVariate \
selects between exactly those two"
),
};
let v_avg = (1.0 - (-kappa * term).exp()) * (v0 - theta) / (kappa * term) + theta;
let r = rho - sigma * freq / (v0 + kappa * theta * term);
let contour_angle = if r * freq < 0.0 {
std::f64::consts::PI / 12.0 * freq.signum()
} else {
0.0
};
let tan_phi = contour_angle.tan();
Ok(ApHelper {
term,
fwd,
strike,
freq,
alpha,
s_alpha,
v_avg,
tan_phi,
phi,
psi,
cpx_log,
chf,
})
}
pub fn evaluate(&self, u: Real) -> Real {
let i = Complex::new(0.0, 1.0);
let h_u = Complex::new(u, u * self.tan_phi - self.alpha);
let h_prime = h_u - i;
let phi_bs = if self.cpx_log == ComplexLogFormula::AsymptoticChF {
(u * Complex::new(1.0, self.tan_phi) * self.phi + self.psi).exp()
} else {
(-0.5
* self.v_avg
* self.term
* (h_prime * h_prime + Complex::new(-h_prime.im, h_prime.re)))
.exp()
};
let chf_val = self.chf.chf(h_prime, self.term);
(-u * self.tan_phi * self.freq).exp()
* (Complex::new(0.0, u * self.freq).exp()
* Complex::new(1.0, self.tan_phi)
* (phi_bs - chf_val)
/ (h_u * h_prime))
.re
* self.s_alpha
}
pub fn control_variate_value(&self) -> QlResult<Real> {
if self.cpx_log == ComplexLogFormula::AsymptoticChF {
require!(self.alpha == -0.5, "alpha must be equal to -0.5");
let phi_freq = Complex::new(self.phi.re, self.phi.im + self.freq);
let ci = ci_complex(-0.5 * phi_freq)?;
let si = si_complex(0.5 * phi_freq)?;
let bracket = -2.0 * ci * (0.5 * phi_freq).sin()
+ (0.5 * phi_freq).cos() * (Complex::new(std::f64::consts::PI, 0.0) + 2.0 * si);
return Ok(self.fwd
- (self.strike * self.fwd).sqrt() / std::f64::consts::PI
* (self.psi.exp() * bracket).re);
}
Ok(BlackCalculator::new(
OptionType::Call,
self.strike,
self.fwd,
(self.v_avg * self.term).sqrt(),
1.0,
)?
.value())
}
}
pub struct AnalyticHestonEngine {
base: OneAssetOptionEngine,
model: SharedMut<HestonModel>,
integration: Integration,
alpha: Real,
}
impl AnalyticHestonEngine {
pub fn new(
model: SharedMut<HestonModel>,
integration_order: Size,
) -> QlResult<AnalyticHestonEngine> {
let integration = Integration::gauss_laguerre(integration_order)?;
let base =
OneAssetOptionEngine::new(OptionArguments::default(), OneAssetOptionResults::default());
base.register_with(model.borrow().calibrated_model().observable());
Ok(AnalyticHestonEngine {
base,
model,
integration,
alpha: -0.5,
})
}
pub fn with_default_order(model: SharedMut<HestonModel>) -> QlResult<AnalyticHestonEngine> {
AnalyticHestonEngine::new(model, 144)
}
pub fn optimal_control_variate(
t: Time,
v0: Real,
kappa: Real,
theta: Real,
sigma: Real,
rho: Real,
) -> ComplexLogFormula {
if t > 0.15
&& (v0 + t * kappa * theta) / sigma * (1.0 - rho * rho).sqrt() < 0.15
&& ((kappa - 0.5 * rho * sigma) * (v0 + t * kappa * theta)
+ kappa * theta * (4.0 * (1.0 - rho * rho)).ln())
/ (sigma * sigma)
< 0.1
{
ComplexLogFormula::AsymptoticChF
} else {
ComplexLogFormula::AngledContour
}
}
}
impl AsObservable for AnalyticHestonEngine {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl PricingEngine for AnalyticHestonEngine {
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,
"not an European option"
);
let Some(payoff) = &arguments.payoff else {
fail!("no payoff given");
};
let payoff: &dyn StrikedTypePayoff = &**payoff;
let Some(payoff) = (payoff as &dyn Any).downcast_ref::<PlainVanillaPayoff>() else {
fail!("non plain vanilla payoff given");
};
let payoff = *payoff;
let maturity_date = exercise.last_date();
let model = self.model.borrow();
let process = model.process();
let kappa = model.kappa();
let sigma = model.sigma();
let theta = model.theta();
let rho = model.rho();
let v0 = model.v0();
drop(model);
let spot = process.s0().current_link()?.value()?;
if spot.is_nan() || spot <= 0.0 {
fail!("negative or null underlying given");
}
let dividend_discount = process
.dividend_yield()
.current_link()?
.discount_date(maturity_date, false)?;
let risk_free_discount_date = process
.risk_free_rate()
.current_link()?
.discount_date(maturity_date, false)?;
let fwd = spot * dividend_discount / risk_free_discount_date;
let time = process.time(&maturity_date)?;
let dr = process
.risk_free_rate()
.current_link()?
.discount(time, false)?;
let strike = payoff.strike();
let c_inf = (1.0 - rho * rho).sqrt() * (v0 + kappa * theta * time) / sigma;
let final_log =
AnalyticHestonEngine::optimal_control_variate(time, v0, kappa, theta, sigma, rho);
let chf = HestonChf::new(kappa, theta, sigma, rho, v0);
let cv_helper = ApHelper::new(time, fwd, strike, final_log, chf, self.alpha)?;
let cv_value = cv_helper.control_variate_value()?;
let h_cv = fwd / std::f64::consts::PI
* self.integration.calculate(c_inf, |u| cv_helper.evaluate(u));
let value = match payoff.option_type() {
OptionType::Call => (cv_value + h_cv) * dr,
OptionType::Put => (cv_value + h_cv - (fwd - strike)) * dr,
};
self.base.results_mut().instrument.value = Some(value);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
const KAPPA: Real = 3.16;
const THETA: Real = 0.09;
const SIGMA: Real = 0.4;
const RHO: Real = -0.2;
const V0: Real = 0.1;
fn fixture() -> HestonChf {
HestonChf::new(KAPPA, THETA, SIGMA, RHO, V0)
}
fn gatheral_ln_chf(chf: &HestonChf, z: Complex, t: Time) -> Complex {
let kappa = chf.kappa;
let theta = chf.theta;
let sigma = chf.sigma;
let rho = chf.rho;
let v0 = chf.v0;
let sigma2 = sigma * sigma;
let i = Complex::new(0.0, 1.0);
let g = Complex::new(kappa, 0.0) - i * rho * sigma * z;
let d = (g * g + sigma2 * z * (z + i)).sqrt();
let g2 = (g - d) / (g + d);
let emdt = (-d * t).exp();
let c = kappa * theta / sigma2
* ((g - d) * t
- 2.0
* ((Complex::new(1.0, 0.0) - g2 * emdt) / (Complex::new(1.0, 0.0) - g2)).ln());
let d_func = (g - d) / sigma2 * (Complex::new(1.0, 0.0) - emdt)
/ (Complex::new(1.0, 0.0) - g2 * emdt);
c + v0 * d_func
}
#[test]
fn ln_chf_matches_gatheral_little_trap() {
let chf = fixture();
let t = 0.5;
for &z in &[
Complex::new(1.5, -0.5),
Complex::new(3.0, 0.0),
Complex::new(2.0, 1.0),
] {
let ported = chf.ln_chf(z, t);
let reference = gatheral_ln_chf(&chf, z, t);
let err = (ported - reference).norm();
assert!(
err < 1e-12,
"lnChF mismatch at z={z:?}: ported={ported:?} reference={reference:?} err={err:e}"
);
}
}
#[test]
fn chf_at_zero_is_one() {
let chf = fixture();
for &t in &[0.1, 0.5, 2.0] {
let value = chf.chf(Complex::new(0.0, 0.0), t);
assert!(
(value.re - 1.0).abs() < 1e-14,
"Re chF(0,{t}) = {}",
value.re
);
assert!(value.im.abs() < 1e-14, "Im chF(0,{t}) = {}", value.im);
}
}
fn small_sigma_fixture(sigma: Real) -> HestonChf {
HestonChf::new(1.0, 0.02, sigma, -0.75, 0.01)
}
#[test]
fn chf_small_sigma_series_matches_exp_lnchf() {
let chf = small_sigma_fixture(1e-6);
let t = 0.5;
for &z in &[Complex::new(1.5, -0.5), Complex::new(3.0, 0.0)] {
let series = chf.chf(z, t);
let closed = chf.ln_chf(z, t).exp();
let err = (series - closed).norm();
assert!(
err < 1e-12,
"series vs exp(lnChF) at z={z:?}: series={series:?} closed={closed:?} err={err:e}"
);
}
}
#[test]
fn chf_small_sigma_series_term2_matches_exp_lnchf() {
let chf = small_sigma_fixture(1e-3);
let t = 0.5;
for &z in &[Complex::new(1.5, -0.5), Complex::new(3.0, 0.0)] {
let series = chf.chf_small_sigma(z, t);
let closed = chf.ln_chf(z, t).exp();
let err = (series - closed).norm();
assert!(
err < 1e-8,
"term2 pin at z={z:?}: series={series:?} closed={closed:?} err={err:e}"
);
}
}
#[test]
fn chf_small_sigma_at_zero_is_one() {
let chf = small_sigma_fixture(1e-6);
for &t in &[0.1, 0.5, 2.0] {
let value = chf.chf(Complex::new(0.0, 0.0), t);
assert!(
(value.re - 1.0).abs() < 1e-14,
"Re chF(0,{t}) = {}",
value.re
);
assert!(value.im.abs() < 1e-14, "Im chF(0,{t}) = {}", value.im);
}
}
#[test]
fn expm1_log1p_helpers_are_load_bearing_in_ln_chf() {
let chf = fixture();
let z = Complex::new(3.0, -0.5);
let rel_gap = |t: Time| -> Real {
let ported = chf.ln_chf(z, t);
let naive = naive_ln_chf(&chf, z, t);
(ported - naive).norm() / ported.norm()
};
let short_rel = rel_gap(1e-10);
let benign_rel = rel_gap(0.5);
assert!(
benign_rel < 1e-13,
"helper and naive lnChF should agree at t=0.5: relative gap={benign_rel:e}"
);
assert!(
short_rel > 1e4 * benign_rel.max(f64::EPSILON),
"helper and naive lnChF should diverge at t=1e-10: short_rel={short_rel:e} benign_rel={benign_rel:e}"
);
}
fn naive_ln_chf(chf: &HestonChf, z: Complex, t: Time) -> Complex {
let kappa = chf.kappa;
let sigma = chf.sigma;
let theta = chf.theta;
let rho = chf.rho;
let v0 = chf.v0;
let sigma2 = sigma * sigma;
let g = Complex::new(kappa, 0.0) + rho * sigma * Complex::new(z.im, -z.re);
let d = (g * g + (z * z + Complex::new(-z.im, z.re)) * sigma2).sqrt();
let mut r = g - d;
if g.re * d.re + g.im * d.im > 0.0 {
r = -sigma2 * z * Complex::new(z.re, z.im + 1.0) / (g + d);
}
let y = if d.re != 0.0 || d.im != 0.0 {
((-d * t).exp() - Complex::new(1.0, 0.0)) / (2.0 * d)
} else {
Complex::new(-0.5 * t, 0.0)
};
let a = kappa * theta / sigma2 * (r * t - 2.0 * (Complex::new(1.0, 0.0) - r * y).ln());
let b = z * Complex::new(z.re, z.im + 1.0) * y / (Complex::new(1.0, 0.0) - r * y);
a + v0 * b
}
const TERM: Time = 0.5;
const FWD: Real = 1.0;
const STRIKE: Real = 1.05;
const ALPHA: Real = -0.5;
fn v_avg() -> Real {
(1.0 - (-KAPPA * TERM).exp()) * (V0 - THETA) / (KAPPA * TERM) + THETA
}
fn helper() -> ApHelper {
ApHelper::new(
TERM,
FWD,
STRIKE,
ComplexLogFormula::AngledContour,
fixture(),
ALPHA,
)
.unwrap()
}
#[test]
fn control_variate_value_matches_black_calculator() {
let reference =
BlackCalculator::new(OptionType::Call, STRIKE, FWD, (v_avg() * TERM).sqrt(), 1.0)
.unwrap()
.value();
let got = helper().control_variate_value().unwrap();
assert!(
(got - reference).abs() < 1e-12,
"controlVariateValue={got} reference={reference}"
);
}
#[test]
fn control_variate_value_discount_is_one_not_risk_free() {
let discounted = BlackCalculator::new(
OptionType::Call,
STRIKE,
FWD,
(v_avg() * TERM).sqrt(),
(-0.0225 * TERM).exp(),
)
.unwrap()
.value();
let got = helper().control_variate_value().unwrap();
assert!(
(got - discounted).abs() > 1e-6,
"discount=1.0 must differ from risk-free-discounted: got={got} discounted={discounted}"
);
}
#[test]
fn control_variate_value_moves_with_v_avg() {
let other = ApHelper::new(
2.0,
FWD,
STRIKE,
ComplexLogFormula::AngledContour,
fixture(),
ALPHA,
)
.unwrap();
let a = helper().control_variate_value().unwrap();
let b = other.control_variate_value().unwrap();
assert!(
(a - b).abs() > 1e-6,
"controlVariateValue should move with vAvg: {a} vs {b}"
);
}
#[test]
fn evaluate_matches_hand_composition() {
let h = helper();
let u = 1.0;
let tan_phi = h.tan_phi;
let freq = h.freq;
let i = Complex::new(0.0, 1.0);
let h_u = Complex::new(u, u * tan_phi - ALPHA);
let h_prime = h_u - i;
let phi_bs =
(-0.5 * v_avg() * TERM * (h_prime * h_prime + Complex::new(-h_prime.im, h_prime.re)))
.exp();
let chf_val = fixture().chf(h_prime, TERM);
let expected = (-u * tan_phi * freq).exp()
* (Complex::new(0.0, u * freq).exp() * Complex::new(1.0, tan_phi) * (phi_bs - chf_val)
/ (h_u * h_prime))
.re
* h.s_alpha;
let got = h.evaluate(u);
assert!(
(got - expected).abs() < 1e-14,
"operator()(1.0)={got} expected={expected}"
);
}
#[test]
fn evaluate_moves_with_u() {
let h = helper();
let a = h.evaluate(0.5);
let b = h.evaluate(2.5);
assert!(
(a - b).abs() > 1e-6,
"operator() should move with u: {a} vs {b}"
);
}
#[test]
fn integration_gauss_laguerre_integrates_x_exp() {
let integration = Integration::gauss_laguerre(64).unwrap();
let got = integration.calculate(1.0, |x| x * (-x).exp());
assert!(
(got - 1.0).abs() < 1e-13,
"int x e^-x dx = {got}, expected 1"
);
}
#[test]
fn integration_evaluations_and_adaptivity() {
let integration = Integration::gauss_laguerre(48).unwrap();
assert_eq!(integration.number_of_evaluations(), 48);
assert!(!integration.is_adaptive_integration());
}
#[test]
fn integration_gauss_laguerre_rejects_high_order() {
assert!(Integration::gauss_laguerre(193).is_err());
}
#[test]
fn ap_helper_deferred_variants_error() {
for cpx in [
ComplexLogFormula::AndersenPiterbarg,
ComplexLogFormula::AndersenPiterbargOptCV,
ComplexLogFormula::AngledContourNoCV,
] {
let err = ApHelper::new(TERM, FWD, STRIKE, cpx, fixture(), ALPHA).unwrap_err();
assert!(err.to_string().contains("deferred"), "{cpx:?}: {err}");
}
}
fn asymptotic_fixture() -> HestonChf {
HestonChf::new(0.2, 0.02, 0.3, -0.75, 0.01)
}
#[test]
fn asymptotic_phi_psi_match_cpp() {
let term: Time = 0.505_555_555_555_555_5;
let helper = ApHelper::new(
term,
1.0,
0.9,
ComplexLogFormula::AsymptoticChF,
asymptotic_fixture(),
ALPHA,
)
.unwrap();
let expected_phi = Complex::new(-0.026_506_508_505_295_25, 0.030055555555555558);
let expected_psi = Complex::new(0.066_615_639_957_623_73, -0.12271634681177794);
assert!(
(helper.phi - expected_phi).norm() < 1e-12,
"phi {:?} vs C++ {expected_phi:?}",
helper.phi
);
assert!(
(helper.psi - expected_psi).norm() < 1e-12,
"psi {:?} vs C++ {expected_psi:?}",
helper.psi
);
}
#[test]
fn asymptotic_control_variate_requires_alpha_minus_half() {
let helper = ApHelper::new(
0.5,
1.0,
0.9,
ComplexLogFormula::AsymptoticChF,
asymptotic_fixture(),
-0.4,
)
.unwrap();
assert!(helper.control_variate_value().is_err());
}
}
#[cfg(test)]
mod engine_tests {
use super::*;
use crate::exercise::{EuropeanExercise, Exercise};
use crate::handle::Handle;
use crate::instrument::Instrument;
use crate::instruments::VanillaOption;
use crate::interestrate::Compounding;
use crate::processes::HestonProcess;
use crate::quotes::make_quote_handle;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
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 optimal_control_variate_selects_angled_contour_for_both_oracle_arms() {
assert_eq!(
AnalyticHestonEngine::optimal_control_variate(0.2492776, 0.1, 3.16, 0.09, 0.4, -0.2),
ComplexLogFormula::AngledContour
);
assert_eq!(
AnalyticHestonEngine::optimal_control_variate(0.6986002, 0.09, 1.2, 0.08, 1.8, -0.45),
ComplexLogFormula::AngledContour
);
}
#[test]
fn optimal_control_variate_flips_to_asymptotic_chf_in_the_asymptotic_regime() {
assert_eq!(
AnalyticHestonEngine::optimal_control_variate(1.0, 0.01, 0.5, 0.01, 2.0, 0.0),
ComplexLogFormula::AsymptoticChF
);
}
fn flat360(rate: Real, reference: Date) -> Handle<dyn YieldTermStructure> {
Handle::new(shared(FlatForward::with_rate(
reference,
rate,
Actual360::new(),
Compounding::Continuous,
Frequency::Annual,
)) as Shared<dyn YieldTermStructure>)
}
#[test]
fn asymptotic_chf_cached_npv_order_96() {
let reference = Date::new(27, Month::December, 2004);
let settings = shared(Settings::new());
settings.set_evaluation_date(reference);
let process = shared(HestonProcess::new(
flat360(0.04, reference),
flat360(0.50, reference),
make_quote_handle(1.0).handle(),
0.01,
0.2,
0.02,
0.3,
-0.75,
));
let model = HestonModel::new(process).unwrap();
let payoff =
shared(PlainVanillaPayoff::new(OptionType::Call, 0.9)) as Shared<dyn StrikedTypePayoff>;
let exercise =
shared(EuropeanExercise::new(Date::new(27, Month::June, 2005))) as Shared<dyn Exercise>;
let mut option = VanillaOption::new(payoff, exercise, Shared::clone(&settings));
let engine = shared_mut(AnalyticHestonEngine::new(model, 96).unwrap())
as SharedMut<dyn PricingEngine>;
option.base_mut().set_pricing_engine(engine);
let npv = option.npv().unwrap();
let expected = 0.00010317795851725543;
assert!(
(npv - expected).abs() < 1e-10,
"AsymptoticChF npv {npv} vs C++ cached {expected} (error {})",
(npv - expected).abs()
);
}
}
#[cfg(test)]
mod engine_oracle {
use super::*;
use crate::exercise::{EuropeanExercise, Exercise};
use crate::handle::Handle;
use crate::instrument::Instrument;
use crate::instruments::VanillaOption;
use crate::interestrate::Compounding;
use crate::processes::HestonProcess;
use crate::quotes::make_quote_handle;
use crate::settings::Settings;
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::date::{Date, Day, Month};
use crate::time::daycounter::DayCounter;
use crate::time::daycounters::actualactual::{ActualActual, Convention};
use crate::time::frequency::Frequency;
fn settlement() -> Date {
Date::new(27, Month::December, 2004)
}
fn isda() -> DayCounter {
ActualActual::with_convention(Convention::ISDA)
}
fn flat(rate: Real) -> Shared<FlatForward> {
shared(FlatForward::with_rate(
settlement(),
rate,
isda(),
Compounding::Continuous,
Frequency::Annual,
))
}
fn handle(curve: &Shared<FlatForward>) -> Handle<dyn YieldTermStructure> {
Handle::new(Shared::clone(curve) as Shared<dyn YieldTermStructure>)
}
#[test]
fn arm1_cached_analytic_price_order_64() {
let settings = shared(Settings::new());
settings.set_evaluation_date(settlement());
let rf = flat(0.0225);
let div = flat(0.02);
let process = shared(HestonProcess::new(
handle(&rf),
handle(&div),
make_quote_handle(1.0).handle(),
0.1,
3.16,
0.09,
0.4,
-0.2,
));
let model = HestonModel::new(process).unwrap();
let payoff = shared(PlainVanillaPayoff::new(OptionType::Call, 1.05))
as Shared<dyn StrikedTypePayoff>;
let exercise = shared(EuropeanExercise::new(Date::new(28, Month::March, 2005)))
as Shared<dyn Exercise>;
let mut option = VanillaOption::new(payoff, exercise, Shared::clone(&settings));
let engine = shared_mut(AnalyticHestonEngine::new(model, 64).unwrap())
as SharedMut<dyn PricingEngine>;
option.base_mut().set_pricing_engine(engine);
let npv = option.npv().unwrap();
let expected = 0.0404774515;
assert!(
(npv - expected).abs() < 1e-8,
"Arm 1: npv {npv} vs cached {expected} (error {})",
(npv - expected).abs()
);
}
#[test]
fn arm2_wilmott_interpolated_prices_default_order() {
let settings = shared(Settings::new());
settings.set_evaluation_date(settlement());
let strikes = [0.9, 1.0, 1.1];
let mut calculated = [0.0; 6];
for (i, calc) in calculated.iter_mut().enumerate() {
let exercise_date = Date::new((8 + i / 3) as Day, Month::September, 2005);
let rf = flat(0.05);
let div = flat(0.02);
let s = rf.discount(0.7, false).unwrap() / div.discount(0.7, false).unwrap();
let process = shared(HestonProcess::new(
handle(&rf),
handle(&div),
make_quote_handle(s).handle(),
0.09,
1.2,
0.08,
1.8,
-0.45,
));
let model = HestonModel::new(process).unwrap();
let payoff = shared(PlainVanillaPayoff::new(OptionType::Call, strikes[i % 3]))
as Shared<dyn StrikedTypePayoff>;
let exercise = shared(EuropeanExercise::new(exercise_date)) as Shared<dyn Exercise>;
let mut option = VanillaOption::new(payoff, exercise, Shared::clone(&settings));
let engine = shared_mut(AnalyticHestonEngine::with_default_order(model).unwrap())
as SharedMut<dyn PricingEngine>;
option.base_mut().set_pricing_engine(engine);
*calc = option.npv().unwrap();
}
let t1 = isda().year_fraction(settlement(), Date::new(8, Month::September, 2005));
let t2 = isda().year_fraction(settlement(), Date::new(9, Month::September, 2005));
let expected = [0.1330371, 0.0641016, 0.0270645];
for i in 0..3 {
let interpolated =
calculated[i] + (calculated[i + 3] - calculated[i]) / (t2 - t1) * (0.7 - t1);
assert!(
(interpolated - expected[i]).abs() < 100.0 * 1e-8,
"Arm 2 strike {}: interpolated {interpolated} vs cached {} (error {})",
strikes[i],
expected[i],
(interpolated - expected[i]).abs()
);
}
}
}