use std::any::Any;
use crate::errors::QlResult;
use crate::instruments::{OneAssetOptionResults, OptionArguments};
use crate::math::randomnumbers::rngtraits::McRngTraits;
use crate::math::randomnumbers::seedgenerator;
use crate::math::statistics::GeneralStatistics;
use crate::math::timegrid::TimeGrid;
use crate::methods::montecarlo::{
ControlVariate, LongstaffSchwartzPathPricer, MonteCarloModel, Path,
};
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: Option<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,
})
}
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 results_mut(&mut self) -> &mut OneAssetOptionResults {
self.base.results_mut()
}
pub fn control_variate(&self) -> bool {
self.base.control_variate()
}
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
.unwrap_or_else(|| calibration_seed(self.base.seed()))
}
pub fn time_grid(&self) -> QlResult<TimeGrid> {
self.base.early_exercise_time_grid()
}
pub fn calculate_with(
&mut self,
pricer: Shared<LongstaffSchwartzPathPricer>,
control_variate: Option<ControlVariate<Path>>,
) -> QlResult<()> {
let grid = self.time_grid()?;
let seed = self.base.resolve_seed();
let seed_calibration = match self.seed_calibration {
Some(0) if RNG::RANDOMIZES_ZERO_SEED => seedgenerator::get_nonzero(),
Some(explicit) => explicit,
None => calibration_seed(seed),
};
let generator = self
.base
.path_generator_on_grid(grid.clone(), Some(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()?;
let generator = self.base.path_generator_on_grid(grid, Some(seed))?;
self.base
.run_with_control_variate(generator, Shared::clone(&pricer), control_variate)?;
self.base.record_seed("seed", seed);
self.base.record_seed("seed_calibration", seed_calibration);
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).max(1)
}
}
#[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> {
seeded_driver(
market,
false,
antithetic_variate,
antithetic_variate_calibration,
n_calibration_samples,
42,
seed_calibration,
)
}
fn seeded_driver(
market: &Market,
brownian_bridge: bool,
antithetic_variate: bool,
antithetic_variate_calibration: Option<bool>,
n_calibration_samples: Option<Size>,
seed: u32,
seed_calibration: Option<u32>,
) -> McLongstaffSchwartzEngineBase<PseudoRandom> {
let mut engine = McLongstaffSchwartzEngineBase::new(
Shared::clone(&market.process) as Shared<dyn StochasticProcess1D>,
Some(4),
None,
brownian_bridge,
antithetic_variate,
false,
Some(512),
None,
None,
seed,
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), None).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), None).unwrap();
let results = instrument_results(&engine);
assert!(results.value.unwrap() > 0.0);
assert!((0.0..=1.0).contains(&pricer.exercise_probability().unwrap()));
}
#[test]
fn the_brownian_bridge_flag_reaches_the_paths() {
let market = flat_market();
let price = |brownian_bridge: bool| {
let mut engine =
seeded_driver(&market, brownian_bridge, false, None, Some(256), 42, None);
let pricer = lsm(&market, &engine);
engine.calculate_with(pricer, None).unwrap();
let results = instrument_results(&engine);
(results.value.unwrap(), results.error_estimate.unwrap())
};
let (plain, plain_error) = price(false);
let (bridged, bridged_error) = price(true);
assert_ne!(plain, bridged, "the bridge must change the paths");
let band = 3.0 * plain_error.hypot(bridged_error);
assert!(
(plain - bridged).abs() < band,
"{plain} vs {bridged} not within {band}"
);
}
fn run(market: &Market, seed: u32, seed_calibration: Option<u32>) -> (u64, u64, Real, Real) {
let mut engine =
seeded_driver(market, false, false, None, Some(64), seed, seed_calibration);
let pricer = lsm(market, &engine);
engine.calculate_with(pricer, None).unwrap();
let results = instrument_results(&engine);
let seed = |key: &str| {
*results.additional_results[key]
.downcast_ref::<Real>()
.unwrap()
};
(
results.value.unwrap().to_bits(),
results.error_estimate.unwrap().to_bits(),
seed("seed"),
seed("seed_calibration"),
)
}
#[test]
fn the_wrapped_calibration_seed_is_never_zero() {
assert_eq!(calibration_seed(u32::MAX - CALIBRATION_SEED_OFFSET + 1), 1);
assert_eq!(calibration_seed(2_526_729_873), 1);
let market = flat_market();
let (_, _, seed, calibration) = run(&market, 2_526_729_873, None);
assert_eq!((seed, calibration), (2_526_729_873.0, 1.0));
}
#[test]
fn a_random_lsm_run_reports_both_seeds_and_replays_bit_for_bit() {
let market = flat_market();
let (npv, error, seed, calibration) = run(&market, 0, None);
assert_ne!(seed, 0.0);
assert_eq!(calibration, Real::from(calibration_seed(seed as u32)));
assert_eq!(
run(&market, seed as u32, None),
(npv, error, seed, calibration)
);
}
#[test]
fn an_explicit_zero_calibration_seed_resolves_at_calculation() {
let market = flat_market();
assert_eq!(
driver(&market, false, None, None, Some(0)).seed_calibration(),
0
);
let (npv, error, seed, calibration) = run(&market, 42, Some(0));
assert_eq!(seed, 42.0);
assert_ne!(calibration, 0.0);
assert_ne!(calibration, Real::from(calibration_seed(42)));
assert_eq!(
run(&market, 42, Some(calibration as u32)),
(npv, error, seed, calibration)
);
}
}