use std::marker::PhantomData;
use crate::errors::QlResult;
use crate::instruments::{OneAssetOptionEngine, OneAssetOptionResults, OptionArguments};
use crate::math::randomnumbers::rngtraits::McRngTraits;
use crate::math::statistics::MeanStdDev;
use crate::math::timegrid::TimeGrid;
use crate::methods::montecarlo::{McSimulation, Path, PathGen, PathGenerator, PathPricer};
use crate::patterns::observable::{AsObservable, Observable};
use crate::pricingengine::{Arguments, Results};
use crate::shared::Shared;
use crate::stochasticprocess::StochasticProcess1D;
use crate::types::{Real, Size};
use crate::{fail, require};
pub struct McVanillaEngineBase<RNG> {
base: OneAssetOptionEngine,
process: Shared<dyn StochasticProcess1D>,
time_steps: Option<Size>,
time_steps_per_year: Option<Size>,
required_samples: Option<Size>,
max_samples: Option<Size>,
required_tolerance: Option<Real>,
brownian_bridge: bool,
antithetic_variate: bool,
control_variate: bool,
seed: u32,
_rng: PhantomData<RNG>,
}
impl<RNG: McRngTraits> McVanillaEngineBase<RNG> {
#[allow(clippy::too_many_arguments)]
pub fn new(
process: Shared<dyn StochasticProcess1D>,
time_steps: Option<Size>,
time_steps_per_year: Option<Size>,
brownian_bridge: bool,
antithetic_variate: bool,
control_variate: bool,
required_samples: Option<Size>,
required_tolerance: Option<Real>,
max_samples: Option<Size>,
seed: u32,
) -> QlResult<Self> {
require!(
time_steps.is_some() || time_steps_per_year.is_some(),
"no time steps provided"
);
require!(
time_steps.is_none() || time_steps_per_year.is_none(),
"both time steps and time steps per year were provided"
);
require!(
time_steps != Some(0),
"timeSteps must be positive, 0 not allowed"
);
require!(
time_steps_per_year != Some(0),
"timeStepsPerYear must be positive, 0 not allowed"
);
let base =
OneAssetOptionEngine::new(OptionArguments::default(), OneAssetOptionResults::default());
base.register_with(process.observable());
Ok(McVanillaEngineBase {
base,
process,
time_steps,
time_steps_per_year,
required_samples,
max_samples,
required_tolerance,
brownian_bridge,
antithetic_variate,
control_variate,
seed,
_rng: PhantomData,
})
}
pub fn arguments(&self) -> &OptionArguments {
self.base.arguments()
}
pub fn arguments_mut(&mut self) -> &mut dyn Arguments {
self.base.arguments_mut()
}
pub fn results(&self) -> &dyn Results {
self.base.results()
}
pub fn reset(&mut self) {
self.base.reset();
}
pub fn observable(&self) -> &Observable {
self.base.observable()
}
pub fn time_grid(&self) -> QlResult<TimeGrid> {
let Some(exercise) = &self.arguments().exercise else {
fail!("no exercise given");
};
let t = self.process.time(&exercise.last_date())?;
if let Some(steps) = self.time_steps {
TimeGrid::new(t, steps)
} else if let Some(per_year) = self.time_steps_per_year {
let steps = (per_year as Real * t) as Size;
TimeGrid::new(t, steps.max(1))
} else {
fail!("time steps not specified");
}
}
pub fn path_generator(&self) -> QlResult<PathGenerator<RNG::RsgType>> {
let grid = self.time_grid()?;
let dimension = grid.size() - 1;
let generator = RNG::make_sequence_generator(dimension, self.seed)?;
PathGenerator::from_time_grid(
Shared::clone(&self.process),
grid,
generator,
self.brownian_bridge,
)
}
pub fn run<P: PathPricer<Path>>(&mut self, path_pricer: P) -> QlResult<()> {
let generator = self.path_generator()?;
self.run_with(generator, path_pricer)
}
pub fn run_with<PG, P>(&mut self, generator: PG, path_pricer: P) -> QlResult<()>
where
PG: PathGen,
P: PathPricer<PG::PathType>,
{
let mut simulation =
McSimulation::<PG, P>::new(self.antithetic_variate, self.control_variate);
simulation.calculate(
generator,
path_pricer,
self.required_tolerance,
self.required_samples,
self.max_samples,
)?;
let mean = simulation.sample_accumulator()?.mean()?;
self.base.results_mut().instrument.value = Some(mean);
if RNG::ALLOWS_ERROR_ESTIMATE {
let error = simulation.error_estimate()?;
self.base.results_mut().instrument.error_estimate = Some(error);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::any::Any;
use super::*;
use crate::exercise::EuropeanExercise;
use crate::handle::{Handle, RelinkableHandle};
use crate::instruments::{PlainVanillaPayoff, StrikedTypePayoff};
use crate::interestrate::Compounding;
use crate::math::randomnumbers::rngtraits::PseudoRandom;
use crate::methods::montecarlo::Path;
use crate::option::OptionType;
use crate::quotes::make_quote_handle;
use crate::shared::{Shared, shared};
use crate::termstructures::volatility::{BlackConstantVol, BlackVolTermStructure};
use crate::termstructures::yields::FlatForward;
use crate::termstructures::yieldtermstructure::YieldTermStructure;
use crate::time::calendars::target::Target;
use crate::time::date::{Date, Month};
use crate::time::daycounters::actual360::Actual360;
use crate::time::frequency::Frequency;
use crate::types::{Rate, Real, Volatility};
const SPOT: Real = 100.0;
const R: Rate = 0.05;
const Q: Rate = 0.02;
const VOL: Volatility = 0.20;
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 gbs_process() -> Shared<dyn StochasticProcess1D> {
let spot = make_quote_handle(SPOT);
let vol = RelinkableHandle::new(shared(BlackConstantVol::new(
reference(),
Some(Target::new()),
VOL,
Actual360::new(),
)) as Shared<dyn BlackVolTermStructure>);
shared(crate::processes::BlackScholesMertonProcess::new(
spot.handle(),
flat_yield(Q),
flat_yield(R),
vol.handle(),
)) as Shared<dyn StochasticProcess1D>
}
fn engine(
time_steps: Option<Size>,
time_steps_per_year: Option<Size>,
required_samples: Option<Size>,
) -> McVanillaEngineBase<PseudoRandom> {
McVanillaEngineBase::new(
gbs_process(),
time_steps,
time_steps_per_year,
false,
false,
false,
required_samples,
None,
None,
42,
)
.unwrap()
}
fn set_option(engine: &mut McVanillaEngineBase<PseudoRandom>, expiry: Date) {
let args = (engine.arguments_mut() as &mut dyn Any)
.downcast_mut::<OptionArguments>()
.unwrap();
args.payoff = Some(shared(PlainVanillaPayoff::new(OptionType::Call, 100.0))
as Shared<dyn StrikedTypePayoff>);
args.exercise =
Some(shared(EuropeanExercise::new(expiry)) as Shared<dyn crate::exercise::Exercise>);
}
#[test]
fn time_grid_uses_fixed_step_count() {
let mut e = engine(Some(12), None, None);
set_option(&mut e, Date::new(15, Month::June, 2027));
assert_eq!(e.time_grid().unwrap().size(), 13);
}
#[test]
fn time_grid_per_year_keeps_at_least_one_step() {
let mut e = engine(None, Some(1), None);
set_option(&mut e, Date::new(15, Month::December, 2026));
assert_eq!(e.time_grid().unwrap().size(), 2);
}
#[test]
fn run_writes_the_mean_and_error_estimate() {
const K: Real = 7.25;
let mut e = engine(Some(4), None, Some(1_000));
set_option(&mut e, Date::new(15, Month::June, 2027));
e.run(|_: &Path| K).unwrap();
let results = (e.results() as &dyn Any)
.downcast_ref::<OneAssetOptionResults>()
.unwrap();
assert_eq!(results.instrument.value, Some(K));
assert!(results.instrument.error_estimate.is_some());
}
#[test]
fn both_time_step_forms_are_rejected() {
assert!(
McVanillaEngineBase::<PseudoRandom>::new(
gbs_process(),
Some(12),
Some(50),
false,
false,
false,
None,
None,
None,
42,
)
.is_err()
);
}
#[test]
fn missing_time_steps_are_rejected() {
assert!(
McVanillaEngineBase::<PseudoRandom>::new(
gbs_process(),
None,
None,
false,
false,
false,
None,
None,
None,
42,
)
.is_err()
);
}
#[test]
fn zero_time_steps_are_rejected() {
assert!(
McVanillaEngineBase::<PseudoRandom>::new(
gbs_process(),
Some(0),
None,
false,
false,
false,
None,
None,
None,
42,
)
.is_err()
);
}
}