use std::any::Any;
use crate::errors::QlResult;
use crate::exercise::ExerciseType;
use crate::instruments::{PlainVanillaPayoff, StrikedTypePayoff, TypePayoff};
use crate::math::randomnumbers::rngtraits::McRngTraits;
use crate::methods::montecarlo::{Path, PathPricer};
use crate::option::OptionType;
use crate::patterns::observable::{AsObservable, Observable};
use crate::payoff::Payoff;
use crate::pricingengine::{Arguments, PricingEngine, Results};
use crate::pricingengines::vanilla::McVanillaEngineBase;
use crate::processes::GeneralizedBlackScholesProcess;
use crate::shared::Shared;
use crate::stochasticprocess::StochasticProcess1D;
use crate::types::{DiscountFactor, Real, Size};
use crate::{fail, require};
pub struct EuropeanPathPricer {
payoff: PlainVanillaPayoff,
discount: DiscountFactor,
}
impl EuropeanPathPricer {
#[allow(clippy::neg_cmp_op_on_partial_ord)]
pub fn new(
option_type: OptionType,
strike: Real,
discount: DiscountFactor,
) -> QlResult<EuropeanPathPricer> {
require!(strike >= 0.0, "strike less than zero not allowed");
Ok(EuropeanPathPricer {
payoff: PlainVanillaPayoff::new(option_type, strike),
discount,
})
}
}
impl PathPricer<Path> for EuropeanPathPricer {
fn price(&self, path: &Path) -> Real {
self.payoff.value(path.back()) * self.discount
}
}
pub struct MCEuropeanEngine<RNG> {
base: McVanillaEngineBase<RNG>,
process: Shared<GeneralizedBlackScholesProcess>,
}
impl<RNG: McRngTraits> MCEuropeanEngine<RNG> {
#[allow(clippy::too_many_arguments)]
pub fn new(
process: Shared<GeneralizedBlackScholesProcess>,
time_steps: Option<Size>,
time_steps_per_year: Option<Size>,
brownian_bridge: bool,
antithetic_variate: bool,
required_samples: Option<Size>,
required_tolerance: Option<Real>,
max_samples: Option<Size>,
seed: u32,
) -> QlResult<MCEuropeanEngine<RNG>> {
let base = McVanillaEngineBase::new(
Shared::clone(&process) as Shared<dyn StochasticProcess1D>,
time_steps,
time_steps_per_year,
brownian_bridge,
antithetic_variate,
false,
required_samples,
required_tolerance,
max_samples,
seed,
)?;
Ok(MCEuropeanEngine { base, process })
}
}
impl<RNG: McRngTraits> AsObservable for MCEuropeanEngine<RNG> {
fn observable(&self) -> &Observable {
self.base.observable()
}
}
impl<RNG: McRngTraits> PricingEngine for MCEuropeanEngine<RNG> {
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");
};
if exercise.exercise_type() != ExerciseType::European {
fail!("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 payoff given");
};
let option_type = payoff.option_type();
let strike = payoff.strike();
let grid = self.base.time_grid()?;
let Some(last_time) = grid.back() else {
fail!("empty time grid");
};
let discount = self
.process
.risk_free_rate()
.current_link()?
.discount(last_time, false)?;
let pricer = EuropeanPathPricer::new(option_type, strike, discount)?;
self.base.run(pricer)
}
}
pub struct MakeMcEuropeanEngine<RNG> {
process: Shared<GeneralizedBlackScholesProcess>,
steps: Option<Size>,
steps_per_year: Option<Size>,
samples: Option<Size>,
max_samples: Option<Size>,
tolerance: Option<Real>,
antithetic: bool,
seed: u32,
_rng: std::marker::PhantomData<RNG>,
}
impl<RNG: McRngTraits> MakeMcEuropeanEngine<RNG> {
pub fn new(process: Shared<GeneralizedBlackScholesProcess>) -> MakeMcEuropeanEngine<RNG> {
MakeMcEuropeanEngine {
process,
steps: None,
steps_per_year: None,
samples: None,
max_samples: None,
tolerance: None,
antithetic: false,
seed: 0,
_rng: std::marker::PhantomData,
}
}
#[must_use]
pub fn with_steps(mut self, steps: Size) -> Self {
self.steps = Some(steps);
self
}
#[must_use]
pub fn with_steps_per_year(mut self, steps: Size) -> Self {
self.steps_per_year = Some(steps);
self
}
#[must_use]
pub fn with_samples(mut self, samples: Size) -> Self {
self.samples = Some(samples);
self
}
#[must_use]
pub fn with_absolute_tolerance(mut self, tolerance: Real) -> Self {
self.tolerance = Some(tolerance);
self
}
#[must_use]
pub fn with_max_samples(mut self, samples: Size) -> Self {
self.max_samples = Some(samples);
self
}
#[must_use]
pub fn with_seed(mut self, seed: u32) -> Self {
self.seed = seed;
self
}
#[must_use]
pub fn with_antithetic_variate(mut self, antithetic: bool) -> Self {
self.antithetic = antithetic;
self
}
pub fn build(self) -> QlResult<MCEuropeanEngine<RNG>> {
require!(
self.steps.is_some() || self.steps_per_year.is_some(),
"number of steps not given"
);
require!(
self.steps.is_none() || self.steps_per_year.is_none(),
"number of steps overspecified"
);
require!(
!(self.samples.is_some() && self.tolerance.is_some()),
"number of samples already set"
);
if self.tolerance.is_some() {
require!(
RNG::ALLOWS_ERROR_ESTIMATE,
"chosen random generator policy does not allow an error estimate"
);
}
require!(!self.antithetic, "antithetic variate not yet supported");
MCEuropeanEngine::new(
self.process,
self.steps,
self.steps_per_year,
false,
false,
self.samples,
self.tolerance,
self.max_samples,
self.seed,
)
}
}
#[cfg(test)]
mod builder_tests {
use super::MakeMcEuropeanEngine;
use crate::math::randomnumbers::rngtraits::PseudoRandom;
use crate::pricingengines::vanilla::test_market::market;
fn maker() -> MakeMcEuropeanEngine<PseudoRandom> {
MakeMcEuropeanEngine::new(market().process)
}
#[test]
fn missing_steps_is_rejected() {
assert!(maker().with_samples(1_000).build().is_err());
}
#[test]
fn overspecified_steps_is_rejected() {
assert!(
maker()
.with_steps(1)
.with_steps_per_year(50)
.with_samples(1_000)
.build()
.is_err()
);
}
#[test]
fn samples_and_tolerance_together_are_rejected() {
assert!(
maker()
.with_steps(1)
.with_samples(1_000)
.with_absolute_tolerance(0.02)
.build()
.is_err()
);
}
#[test]
fn antithetic_variate_is_rejected_as_deferred() {
assert!(
maker()
.with_steps(1)
.with_samples(1_000)
.with_antithetic_variate(true)
.build()
.is_err()
);
}
#[test]
fn plain_pseudo_config_builds() {
assert!(
maker()
.with_steps(1)
.with_samples(1_000)
.with_seed(42)
.build()
.is_ok()
);
}
}
#[cfg(test)]
mod oracle {
use super::MakeMcEuropeanEngine;
use crate::exercise::EuropeanExercise;
use crate::instrument::Instrument;
use crate::instruments::{EuropeanOption, PlainVanillaPayoff};
use crate::math::randomnumbers::rngtraits::PseudoRandom;
use crate::option::OptionType;
use crate::pricingengine::PricingEngine;
use crate::pricingengines::vanilla::test_market::{Market, market, time_to_days, today};
use crate::shared::{Shared, SharedMut, shared, shared_mut};
use crate::time::date::Date;
use crate::types::Real;
fn mc_option(market: &Market, strike: Real, expiry: Date, seed: u32) -> EuropeanOption {
let payoff = shared(PlainVanillaPayoff::new(OptionType::Call, strike));
let exercise = shared(EuropeanExercise::new(expiry));
let mut option = EuropeanOption::new(payoff, exercise, Shared::clone(&market.settings));
let engine = shared_mut(
MakeMcEuropeanEngine::<PseudoRandom>::new(Shared::clone(&market.process))
.with_steps(1)
.with_samples(40_000)
.with_seed(seed)
.build()
.unwrap(),
);
option
.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
option
}
#[test]
fn mc_matches_analytic_across_moneyness() {
let market = market();
market.set(100.0, 0.02, 0.05, 0.20);
let expiry = today() + time_to_days(1.0);
for &strike in &[90.0, 100.0, 110.0] {
let analytic = market
.option(OptionType::Call, strike, expiry)
.npv()
.unwrap();
let mut mc = mc_option(&market, strike, expiry, 42);
let mc_npv = mc.npv().unwrap();
let se = mc.error_estimate().unwrap();
assert!(se > 0.0, "K={strike}: error estimate must be positive");
let diff = (mc_npv - analytic).abs();
assert!(
diff < 3.0 * se,
"K={strike}: |mc - analytic|={diff} not within 3*se={}",
3.0 * se
);
}
}
#[test]
fn same_seed_is_bitwise_deterministic() {
let market = market();
market.set(100.0, 0.02, 0.05, 0.20);
let expiry = today() + time_to_days(1.0);
let first = mc_option(&market, 100.0, expiry, 42).npv().unwrap();
let second = mc_option(&market, 100.0, expiry, 42).npv().unwrap();
assert_eq!(first, second, "seed 42 must reproduce the NPV bitwise");
let other = mc_option(&market, 100.0, expiry, 43).npv().unwrap();
assert_ne!(first, other, "a different seed must change the NPV");
}
#[test]
fn tolerance_path_converges() {
let market = market();
market.set(100.0, 0.02, 0.05, 0.20);
let expiry = today() + time_to_days(1.0);
let payoff = shared(PlainVanillaPayoff::new(OptionType::Call, 100.0));
let exercise = shared(EuropeanExercise::new(expiry));
let mut option = EuropeanOption::new(payoff, exercise, Shared::clone(&market.settings));
let engine = shared_mut(
MakeMcEuropeanEngine::<PseudoRandom>::new(Shared::clone(&market.process))
.with_steps(1)
.with_absolute_tolerance(0.05)
.with_seed(42)
.build()
.unwrap(),
);
option
.base_mut()
.set_pricing_engine(engine as SharedMut<dyn PricingEngine>);
let mc_npv = option.npv().unwrap();
let se = option.error_estimate().unwrap();
let analytic = market
.option(OptionType::Call, 100.0, expiry)
.npv()
.unwrap();
assert!(se <= 0.05, "tolerance mode must drive se={se} below 0.05");
assert!(
(mc_npv - analytic).abs() < 3.0 * se,
"tolerance run must land within 3*se of analytic"
);
}
}