use std::any::Any;
use crate::errors::QlResult;
use crate::instruments::OptionArguments;
use crate::math::randomnumbers::rngtraits::McRngTraits;
use crate::math::statistics::GeneralStatistics;
use crate::math::timegrid::TimeGrid;
use crate::methods::montecarlo::{LongstaffSchwartzPathPricer, MonteCarloModel};
use crate::patterns::observable::Observable;
use crate::pricingengine::{Arguments, Results};
use crate::pricingengines::vanilla::McVanillaEngineBase;
use crate::shared::{Shared, shared};
use crate::stochasticprocess::StochasticProcess1D;
use crate::types::{Real, Size};
const DEFAULT_CALIBRATION_SAMPLES: Size = 2048;
const CALIBRATION_SEED_OFFSET: u32 = 1_768_237_423;
pub struct McLongstaffSchwartzEngineBase<RNG> {
base: McVanillaEngineBase<RNG>,
n_calibration_samples: Size,
antithetic_variate_calibration: bool,
seed_calibration: u32,
}
impl<RNG: McRngTraits> McLongstaffSchwartzEngineBase<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,
n_calibration_samples: Option<Size>,
antithetic_variate_calibration: Option<bool>,
seed_calibration: Option<u32>,
) -> QlResult<Self> {
let base = McVanillaEngineBase::new(
process,
time_steps,
time_steps_per_year,
brownian_bridge,
antithetic_variate,
control_variate,
required_samples,
required_tolerance,
max_samples,
seed,
)?;
Ok(McLongstaffSchwartzEngineBase {
base,
n_calibration_samples: n_calibration_samples.unwrap_or(DEFAULT_CALIBRATION_SAMPLES),
antithetic_variate_calibration: antithetic_variate_calibration
.unwrap_or(antithetic_variate),
seed_calibration: seed_calibration.unwrap_or_else(|| calibration_seed(seed)),
})
}
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 n_calibration_samples(&self) -> Size {
self.n_calibration_samples
}
pub fn antithetic_variate_calibration(&self) -> bool {
self.antithetic_variate_calibration
}
pub fn seed_calibration(&self) -> u32 {
self.seed_calibration
}
pub fn time_grid(&self) -> QlResult<TimeGrid> {
self.base.time_grid()
}
pub fn calculate_with(&mut self, pricer: Shared<LongstaffSchwartzPathPricer>) -> QlResult<()> {
let generator = self.base.path_generator_with_seed(self.seed_calibration)?;
let mut calibration_model = MonteCarloModel::new(
generator,
Shared::clone(&pricer),
GeneralStatistics::default(),
self.antithetic_variate_calibration,
)?;
calibration_model.add_samples(self.n_calibration_samples)?;
pricer.calibrate()?;
self.base.run(Shared::clone(&pricer))?;
let probability = pricer.exercise_probability()?;
self.base
.results_mut()
.instrument
.additional_results
.insert(
"exerciseProbability".to_string(),
shared(probability) as Shared<dyn Any>,
);
Ok(())
}
}
fn calibration_seed(seed: u32) -> u32 {
if seed == 0 {
0
} else {
seed.wrapping_add(CALIBRATION_SEED_OFFSET)
}
}
#[cfg(test)]
mod tests {
use std::any::Any;
use super::*;
use crate::exercise::{EuropeanExercise, Exercise};
use crate::instrument::InstrumentResults;
use crate::instruments::{OneAssetOptionResults, PlainVanillaPayoff, StrikedTypePayoff};
use crate::math::randomnumbers::rngtraits::PseudoRandom;
use crate::methods::montecarlo::{
EarlyExercisePathPricer, LsmBasisSystem, Path, PolynomialType,
};
use crate::option::OptionType;
use crate::pricingengines::vanilla::test_market::{Market, market, time_to_days, today};
use crate::shared::shared;
const STRIKE: Real = 100.0;
fn flat_market() -> Market {
let market = market();
market.set(STRIKE, 0.0, 0.05, 0.20);
market
}
struct AmericanPut;
impl EarlyExercisePathPricer<Path> for AmericanPut {
type State = Real;
fn value(&self, path: &Path, t: Size) -> Real {
(STRIKE - path[t]).max(0.0)
}
fn state(&self, path: &Path, t: Size) -> Real {
path[t]
}
fn basis_system(&self) -> Vec<Box<dyn Fn(Real) -> Real>> {
LsmBasisSystem::path_basis_system(2, PolynomialType::Monomial)
}
}
fn driver(
market: &Market,
antithetic_variate: bool,
antithetic_variate_calibration: Option<bool>,
n_calibration_samples: Option<Size>,
seed_calibration: Option<u32>,
) -> McLongstaffSchwartzEngineBase<PseudoRandom> {
let mut engine = McLongstaffSchwartzEngineBase::new(
Shared::clone(&market.process) as Shared<dyn StochasticProcess1D>,
Some(4),
None,
false,
antithetic_variate,
false,
Some(512),
None,
None,
42,
n_calibration_samples,
antithetic_variate_calibration,
seed_calibration,
)
.unwrap();
let args = (engine.arguments_mut() as &mut dyn Any)
.downcast_mut::<OptionArguments>()
.unwrap();
args.payoff = Some(shared(PlainVanillaPayoff::new(OptionType::Put, STRIKE))
as Shared<dyn StrikedTypePayoff>);
args.exercise = Some(
shared(EuropeanExercise::new(today() + time_to_days(1.0))) as Shared<dyn Exercise>
);
engine
}
fn lsm(
market: &Market,
engine: &McLongstaffSchwartzEngineBase<PseudoRandom>,
) -> Shared<LongstaffSchwartzPathPricer> {
shared(
LongstaffSchwartzPathPricer::new(
&engine.time_grid().unwrap(),
shared(AmericanPut) as Shared<dyn EarlyExercisePathPricer<Path, State = Real>>,
&market.process.risk_free_rate(),
)
.unwrap(),
)
}
fn instrument_results(
engine: &McLongstaffSchwartzEngineBase<PseudoRandom>,
) -> &InstrumentResults {
&(engine.results() as &dyn Any)
.downcast_ref::<OneAssetOptionResults>()
.unwrap()
.instrument
}
#[test]
fn the_calibration_seed_offsets_every_nonzero_pricing_seed() {
assert_eq!(calibration_seed(0), 0);
assert_eq!(calibration_seed(42), 42 + 1_768_237_423);
assert_eq!(calibration_seed(1), 1_768_237_424);
let market = flat_market();
assert_eq!(
driver(&market, false, None, None, None).seed_calibration(),
1_768_237_465
);
}
#[test]
fn the_calibration_defaults_follow_the_cpp_fallbacks() {
let market = flat_market();
let defaulted = driver(&market, true, None, None, None);
assert_eq!(defaulted.n_calibration_samples(), 2048);
assert!(
defaulted.antithetic_variate_calibration(),
"an unset calibration flag follows the pricing one"
);
let explicit = driver(&market, true, Some(false), Some(64), Some(7));
assert_eq!(explicit.n_calibration_samples(), 64);
assert!(!explicit.antithetic_variate_calibration());
assert_eq!(explicit.seed_calibration(), 7);
}
#[test]
fn both_passes_run_and_fill_the_results() {
let market = flat_market();
let mut engine = driver(&market, false, None, Some(256), None);
let pricer = lsm(&market, &engine);
engine.calculate_with(Shared::clone(&pricer)).unwrap();
let results = instrument_results(&engine);
let value = results.value.unwrap();
assert!(value > 0.0, "a calibrated American put is worth something");
assert!(results.error_estimate.unwrap() > 0.0);
let probability = *results.additional_results["exerciseProbability"]
.downcast_ref::<Real>()
.unwrap();
assert!((0.0..=1.0).contains(&probability));
assert_eq!(probability, pricer.exercise_probability().unwrap());
}
#[test]
fn antithetic_calibration_doubles_the_buffered_paths() {
let market = flat_market();
let mut engine = driver(&market, false, Some(true), Some(16), None);
let pricer = lsm(&market, &engine);
engine.calculate_with(Shared::clone(&pricer)).unwrap();
let results = instrument_results(&engine);
assert!(results.value.unwrap() > 0.0);
assert!((0.0..=1.0).contains(&pricer.exercise_probability().unwrap()));
}
}